├── .discourse-compatibility ├── .github └── workflows │ └── discourse-theme.yml ├── .gitignore ├── .npmrc ├── .prettierrc.cjs ├── .rubocop.yml ├── .streerc ├── .template-lintrc.cjs ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md ├── about.json ├── assets └── sprite.svg ├── common └── common.scss ├── eslint.config.mjs ├── javascripts └── discourse │ ├── components │ ├── toc-contents.gjs │ ├── toc-heading.gjs │ ├── toc-large-buttons.gjs │ ├── toc-mini-buttons.gjs │ ├── toc-mini.gjs │ ├── toc-timeline.gjs │ └── toc-toggle.gjs │ ├── connectors │ ├── below-docs-topic │ │ └── d-toc-wrapper.gjs │ └── topic-navigation │ │ └── d-toc-wrapper.gjs │ ├── initializers │ ├── disco-toc-composer.js │ ├── init-toc-mini.js │ └── init-toc-toggle.js │ └── services │ └── toc-processor.js ├── locales ├── ar.yml ├── be.yml ├── bg.yml ├── bs_BA.yml ├── ca.yml ├── cs.yml ├── da.yml ├── de.yml ├── el.yml ├── en.yml ├── en_GB.yml ├── es.yml ├── et.yml ├── fa_IR.yml ├── fi.yml ├── fr.yml ├── gl.yml ├── he.yml ├── hr.yml ├── hu.yml ├── hy.yml ├── id.yml ├── it.yml ├── ja.yml ├── ko.yml ├── lt.yml ├── lv.yml ├── nb_NO.yml ├── nl.yml ├── pl_PL.yml ├── pt.yml ├── pt_BR.yml ├── ro.yml ├── ru.yml ├── sk.yml ├── sl.yml ├── sq.yml ├── sr.yml ├── sv.yml ├── sw.yml ├── te.yml ├── th.yml ├── tr_TR.yml ├── ug.yml ├── uk.yml ├── ur.yml ├── vi.yml ├── zh_CN.yml └── zh_TW.yml ├── package.json ├── pnpm-lock.yaml ├── settings.yml ├── spec └── system │ ├── core_features_spec.rb │ ├── discotoc_author_spec.rb │ ├── discotoc_progress_user_spec.rb │ └── discotoc_timeline_user_spec.rb ├── stylelint.config.mjs └── translator.yml /.discourse-compatibility: -------------------------------------------------------------------------------- 1 | < 3.5.0.beta5-dev: 8bd98f0288a152b4e06a46dbb3bb73e6bc26f013 2 | < 3.5.0.beta1-dev: 05d454d1dbea9688d76ae037c5b7077f75c15fea 3 | < 3.4.0.beta2-dev: f1d183eaac44b647978cdd481ec06407f4008d7c 4 | < 3.4.0.beta1-dev: 51f099289db87d3f0e5fe89298afaeaf899bebc7 5 | < 3.3.0.beta1-dev: 3179e886a366e15fb0de3c869990c2292763bd89 6 | < 3.2.0.beta2: 0f2a0e73e6c2924f2b44d3241931f2bd5f77a9ae 7 | 3.1.999: 323bd485b08889360edcae826d6272fd8e77d180 8 | 2.7.13: 5b2f5a455e1adf8ce5e8c1cfb7fbc3c388d3d82a 9 | 2.6.0.beta3: 68d40fe9f5b625cf465adc31b502a54e16d02cc6 10 | -------------------------------------------------------------------------------- /.github/workflows/discourse-theme.yml: -------------------------------------------------------------------------------- 1 | name: Discourse Theme 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | ci: 11 | uses: discourse/.github/.github/workflows/discourse-theme.yml@v1 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .discourse-site 3 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict = true 2 | auto-install-peers = false 3 | -------------------------------------------------------------------------------- /.prettierrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = require("@discourse/lint-configs/prettier"); 2 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_gem: 2 | rubocop-discourse: stree-compat.yml 3 | -------------------------------------------------------------------------------- /.streerc: -------------------------------------------------------------------------------- 1 | --print-width=100 2 | --plugins=plugin/trailing_comma,plugin/disable_auto_ternary 3 | -------------------------------------------------------------------------------- /.template-lintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = require("@discourse/lint-configs/template-lint"); 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | group :development do 6 | gem "rubocop-discourse" 7 | gem "syntax_tree" 8 | end 9 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | activesupport (8.0.2) 5 | base64 6 | benchmark (>= 0.3) 7 | bigdecimal 8 | concurrent-ruby (~> 1.0, >= 1.3.1) 9 | connection_pool (>= 2.2.5) 10 | drb 11 | i18n (>= 1.6, < 2) 12 | logger (>= 1.4.2) 13 | minitest (>= 5.1) 14 | securerandom (>= 0.3) 15 | tzinfo (~> 2.0, >= 2.0.5) 16 | uri (>= 0.13.1) 17 | ast (2.4.3) 18 | base64 (0.2.0) 19 | benchmark (0.4.0) 20 | bigdecimal (3.2.0) 21 | concurrent-ruby (1.3.5) 22 | connection_pool (2.5.3) 23 | drb (2.2.3) 24 | i18n (1.14.7) 25 | concurrent-ruby (~> 1.0) 26 | json (2.12.2) 27 | language_server-protocol (3.17.0.5) 28 | lint_roller (1.1.0) 29 | logger (1.7.0) 30 | minitest (5.25.5) 31 | parallel (1.27.0) 32 | parser (3.3.8.0) 33 | ast (~> 2.4.1) 34 | racc 35 | prettier_print (1.2.1) 36 | prism (1.4.0) 37 | racc (1.8.1) 38 | rack (3.1.15) 39 | rainbow (3.1.1) 40 | regexp_parser (2.10.0) 41 | rubocop (1.75.8) 42 | json (~> 2.3) 43 | language_server-protocol (~> 3.17.0.2) 44 | lint_roller (~> 1.1.0) 45 | parallel (~> 1.10) 46 | parser (>= 3.3.0.2) 47 | rainbow (>= 2.2.2, < 4.0) 48 | regexp_parser (>= 2.9.3, < 3.0) 49 | rubocop-ast (>= 1.44.0, < 2.0) 50 | ruby-progressbar (~> 1.7) 51 | unicode-display_width (>= 2.4.0, < 4.0) 52 | rubocop-ast (1.44.1) 53 | parser (>= 3.3.7.2) 54 | prism (~> 1.4) 55 | rubocop-capybara (2.22.1) 56 | lint_roller (~> 1.1) 57 | rubocop (~> 1.72, >= 1.72.1) 58 | rubocop-discourse (3.12.1) 59 | activesupport (>= 6.1) 60 | lint_roller (>= 1.1.0) 61 | rubocop (>= 1.73.2) 62 | rubocop-capybara (>= 2.22.0) 63 | rubocop-factory_bot (>= 2.27.0) 64 | rubocop-rails (>= 2.30.3) 65 | rubocop-rspec (>= 3.0.1) 66 | rubocop-rspec_rails (>= 2.31.0) 67 | rubocop-factory_bot (2.27.1) 68 | lint_roller (~> 1.1) 69 | rubocop (~> 1.72, >= 1.72.1) 70 | rubocop-rails (2.32.0) 71 | activesupport (>= 4.2.0) 72 | lint_roller (~> 1.1) 73 | rack (>= 1.1) 74 | rubocop (>= 1.75.0, < 2.0) 75 | rubocop-ast (>= 1.44.0, < 2.0) 76 | rubocop-rspec (3.6.0) 77 | lint_roller (~> 1.1) 78 | rubocop (~> 1.72, >= 1.72.1) 79 | rubocop-rspec_rails (2.31.0) 80 | lint_roller (~> 1.1) 81 | rubocop (~> 1.72, >= 1.72.1) 82 | rubocop-rspec (~> 3.5) 83 | ruby-progressbar (1.13.0) 84 | securerandom (0.4.1) 85 | syntax_tree (6.2.0) 86 | prettier_print (>= 1.2.0) 87 | tzinfo (2.0.6) 88 | concurrent-ruby (~> 1.0) 89 | unicode-display_width (3.1.4) 90 | unicode-emoji (~> 4.0, >= 4.0.4) 91 | unicode-emoji (4.0.4) 92 | uri (1.0.3) 93 | 94 | PLATFORMS 95 | ruby 96 | 97 | DEPENDENCIES 98 | rubocop-discourse 99 | syntax_tree 100 | 101 | BUNDLED WITH 102 | 2.6.9 103 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Civilized Discourse Construction Kit, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DiscoTOC 2 | 3 | A Discourse theme component that generates a table of contents for topics with one click 4 | 5 | https://meta.discourse.org/t/discotoc-automatic-table-of-contents/111143 6 | -------------------------------------------------------------------------------- /about.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "DiscoTOC", 3 | "component": true, 4 | "about_url": "https://meta.discourse.org/t/discotoc-automatic-table-of-contents/111143", 5 | "license_url": "https://github.com/discourse/DiscoTOC/blob/main/LICENSE", 6 | "theme_version": "2.1.0", 7 | "assets": { 8 | "icons-sprite": "/assets/sprite.svg" 9 | }, 10 | "modifiers": { 11 | "svg_icons": ["hashtag"] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /assets/sprite.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 12 | 13 | 14 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /common/common.scss: -------------------------------------------------------------------------------- 1 | $padding-basis: 0.75em; 2 | 3 | @media screen and (width >= 925px) { 4 | .container.posts { 5 | // needs to be static, otherwise we get content shifts when the TOC shows/hides 6 | grid-template-columns: 75% 25%; 7 | } 8 | } 9 | 10 | .d-toc-main { 11 | min-width: 6em; 12 | max-width: 13em; 13 | word-wrap: break-word; 14 | box-sizing: border-box; 15 | 16 | .overlay & { 17 | max-width: 100%; 18 | } 19 | 20 | a { 21 | display: block; 22 | padding: 0.15em 0; 23 | color: var(--primary-medium); 24 | 25 | &.scroll-to-bottom { 26 | padding-left: $padding-basis; 27 | } 28 | } 29 | 30 | .timeline-toggle { 31 | margin-top: 1em; 32 | } 33 | 34 | #d-toc { 35 | border-left: 1px solid var(--primary-low); 36 | max-height: calc(100vh - 4.5em - var(--header-offset)); 37 | overflow: auto; 38 | 39 | ul { 40 | list-style-type: none; 41 | margin: 0; 42 | padding: 0; 43 | } 44 | 45 | li.d-toc-item { 46 | margin: 0; 47 | padding: 0; 48 | padding-left: $padding-basis; 49 | line-height: var(--line-height-large); 50 | 51 | > ul { 52 | max-height: 0; 53 | overflow: hidden; 54 | opacity: 0.5; 55 | transition: 56 | opacity 0.3s ease-in-out, 57 | max-height 0.3s ease-in-out; 58 | } 59 | 60 | &.active, 61 | .d-toc-wrapper.overlay & { 62 | ul { 63 | max-height: 500em; 64 | overflow: visible; 65 | opacity: 1; 66 | animation: hide-scroll 0.3s backwards; 67 | } 68 | 69 | // hides the scrollbar while subsection expands 70 | @keyframes hide-scroll { 71 | from, 72 | to { 73 | overflow: hidden; 74 | } 75 | } 76 | } 77 | 78 | > a:hover { 79 | color: var(--primary-high); 80 | } 81 | 82 | &.direct-active > a { 83 | position: relative; 84 | color: var(--primary); 85 | 86 | &::before { 87 | content: ""; 88 | width: 1px; 89 | margin-top: -1px; 90 | background-color: var(--tertiary); 91 | position: absolute; 92 | height: 100%; 93 | } 94 | } 95 | } 96 | 97 | > ul > li > ul { 98 | font-size: var(--font-down-1); 99 | 100 | > li:first-child { 101 | padding-top: 0.25em; 102 | } 103 | 104 | > li { 105 | padding-bottom: 0.15em; 106 | } 107 | 108 | li.direct-active > a::before { 109 | // it's odd that we need this 110 | margin-left: -1px; 111 | } 112 | 113 | li.d-toc-h2 ~ li.d-toc-h3, 114 | li.d-toc-h2 ~ li.d-toc-h4, 115 | li.d-toc-h2 ~ li.d-toc-h5, 116 | li.d-toc-h3 ~ li.d-toc-h4, 117 | li.d-toc-h3 ~ li.d-toc-h5, 118 | li.d-toc-h4 ~ li.d-toc-h5 { 119 | > a { 120 | padding-left: $padding-basis; 121 | } 122 | } 123 | } 124 | } 125 | } 126 | 127 | // active line marker 128 | $selector: "> ul > li.direct-active > a:before"; 129 | $map: ( 130 | "left": "html:not(.rtl)", 131 | "right": "html.rtl", 132 | ); 133 | 134 | /* 135 | // loop below generates styling for non-RTL and RTL 136 | // Example: 137 | html:not(.rtl) SELECTOR { 138 | left: -.75em 139 | } 140 | html.rtl SELECTOR { 141 | right: -.75em 142 | } 143 | */ 144 | @each $prop, $parent in $map { 145 | #{$parent} #d-toc { 146 | #{$selector} { 147 | #{$prop}: (-$padding-basis); 148 | } 149 | > ul > li #{$selector} { 150 | #{$prop}: (-$padding-basis) * 2; 151 | } 152 | } 153 | } 154 | 155 | // END active line marker 156 | .d-toc-mini { 157 | height: 100%; 158 | 159 | button { 160 | height: 100%; 161 | } 162 | } 163 | 164 | // overlaid timeline (on mobile and narrow screens) 165 | .topic-navigation.with-topic-progress { 166 | .d-toc-wrapper { 167 | position: fixed; 168 | margin-top: 0.25em; 169 | height: calc(100vh - 50px - var(--header-offset)); 170 | opacity: 0.5; 171 | right: -100vw; 172 | top: var(--header-offset); 173 | width: 75vw; 174 | max-width: 350px; 175 | background-color: var(--secondary); 176 | box-shadow: var(--shadow-dropdown); 177 | z-index: z("modal", "overlay"); 178 | transition: all 0.2s ease-in-out; 179 | 180 | .d-toc-main { 181 | width: 100%; 182 | padding: 0.5em; 183 | height: 100%; 184 | 185 | #d-toc { 186 | max-height: calc(100% - 3em); 187 | } 188 | } 189 | 190 | &.overlay { 191 | right: 0; 192 | width: 75vw; 193 | opacity: 1; 194 | 195 | .d-toc-main #d-toc li.d-toc-item ul { 196 | transition: none; 197 | } 198 | } 199 | 200 | a.scroll-to-bottom, 201 | a.d-toc-close { 202 | display: inline-block; 203 | padding: 0.5em; 204 | } 205 | 206 | .d-toc-icons { 207 | text-align: right; 208 | } 209 | } 210 | } 211 | 212 | // core sets first child's top margin to 0 213 | // ensure it's also 0 when TOC markup is first 214 | .cooked > div[data-theme-toc]:first-child + * { 215 | margin-top: 0; 216 | } 217 | 218 | // RTL Support 219 | .rtl { 220 | .d-toc-main { 221 | border-left: none; 222 | border-right: 1px solid var(--primary-low); 223 | 224 | #d-toc li.d-toc-item, 225 | a.scroll-to-bottom { 226 | padding-left: 0; 227 | padding-right: $padding-basis; 228 | } 229 | } 230 | 231 | .topic-navigation.with-topic-progress .d-toc-wrapper { 232 | right: unset; 233 | left: -100vw; 234 | 235 | &.overlay { 236 | right: unset; 237 | left: 0; 238 | } 239 | } 240 | } 241 | 242 | // Composer preview notice 243 | .edit-title .d-editor-preview [data-theme-toc], 244 | body.toc-for-replies-enabled .d-editor-preview [data-theme-toc] { 245 | background: var(--tertiary); 246 | color: var(--secondary); 247 | position: sticky; 248 | z-index: 1; 249 | top: 0; 250 | height: 30px; 251 | display: flex; 252 | align-items: center; 253 | justify-content: center; 254 | 255 | &::before { 256 | content: "#{$composer_toc_text}"; 257 | } 258 | } 259 | 260 | // Docs plugin outlet 261 | .below-docs-topic-outlet.d-toc-wrapper { 262 | position: sticky; 263 | top: calc(var(--header-offset, 60px) + 1em); 264 | max-height: calc(100vh - 2em - var(--header-offset, 60px)); 265 | 266 | .mobile-view & { 267 | display: none; 268 | } 269 | 270 | .d-toc-main { 271 | display: block; 272 | } 273 | } 274 | 275 | // toggle in timeline 276 | .timeline-container 277 | .topic-timeline 278 | .timeline-footer-controls 279 | button:last-child { 280 | // annoying core style 281 | &.timeline-toggle { 282 | margin-right: 100%; 283 | white-space: nowrap; 284 | } 285 | } 286 | 287 | // jump to bottom in timeline 288 | .d-toc-footer-icons { 289 | font-size: var(--font-down-1); 290 | margin-top: 0.5em; 291 | 292 | button { 293 | color: var(--tertiary); 294 | 295 | .d-icon { 296 | color: currentcolor; 297 | } 298 | } 299 | } 300 | 301 | // on shorter screens, we can keep this consistently in the same location 302 | // this is kind of far away for tall screens, so the more variable position below might be better 303 | @media screen and (height <= 950px) { 304 | .timeline-toggle { 305 | position: fixed; 306 | bottom: 0; 307 | } 308 | } 309 | 310 | // hides the timeline when d-toc is shown 311 | .d-toc-active { 312 | .timeline-container { 313 | display: none; 314 | } 315 | } 316 | 317 | // hide the toggle in the expanded timeline on mobile 318 | .timeline-fullscreen { 319 | .timeline-toggle { 320 | display: none; 321 | } 322 | } 323 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import DiscourseRecommendedTheme from "@discourse/lint-configs/eslint-theme"; 2 | 3 | export default [...DiscourseRecommendedTheme]; 4 | -------------------------------------------------------------------------------- /javascripts/discourse/components/toc-contents.gjs: -------------------------------------------------------------------------------- 1 | import Component from "@glimmer/component"; 2 | import { tracked } from "@glimmer/tracking"; 3 | import { action } from "@ember/object"; 4 | import didInsert from "@ember/render-modifiers/modifiers/did-insert"; 5 | import didUpdate from "@ember/render-modifiers/modifiers/did-update"; 6 | import { service } from "@ember/service"; 7 | import { debounce } from "discourse/lib/decorators"; 8 | import { headerOffset } from "discourse/lib/offset-calculator"; 9 | import TocHeading from "../components/toc-heading"; 10 | import TocLargeButtons from "../components/toc-large-buttons"; 11 | import TocMiniButtons from "../components/toc-mini-buttons"; 12 | 13 | const POSITION_BUFFER = 150; 14 | const SCROLL_DEBOUNCE = 50; 15 | const RESIZE_DEBOUNCE = 200; 16 | 17 | export default class TocContents extends Component { 18 | @service tocProcessor; 19 | @service appEvents; 20 | 21 | @tracked activeHeadingId = null; 22 | @tracked headingPositions = []; 23 | @tracked activeAncestorIds = []; 24 | 25 | willDestroy() { 26 | super.willDestroy(...arguments); 27 | window.removeEventListener("scroll", this.updateActiveHeadingOnScroll); 28 | window.removeEventListener("resize", this.calculateHeadingPositions); 29 | this.appEvents.off( 30 | "topic:current-post-changed", 31 | this.calculateHeadingPositions 32 | ); 33 | } 34 | 35 | get mappedToc() { 36 | return this.mappedTocStructure(this.args.tocStructure); 37 | } 38 | 39 | @action 40 | setup() { 41 | this.listenForScroll(); 42 | this.listenForPostChange(); 43 | this.listenForResize(); 44 | this.updateHeadingPositions(); 45 | this.updateActiveHeadingOnScroll(); // manual on setup so active class is added 46 | } 47 | 48 | @action 49 | listenForScroll() { 50 | window.addEventListener("scroll", this.updateActiveHeadingOnScroll); 51 | } 52 | 53 | @action 54 | listenForResize() { 55 | // due to text reflow positions will change after significant resize 56 | window.addEventListener("resize", this.calculateHeadingPositions); 57 | } 58 | 59 | @action 60 | listenForPostChange() { 61 | this.appEvents.on( 62 | "topic:current-post-changed", 63 | this.calculateHeadingPositions 64 | ); 65 | } 66 | 67 | @debounce(RESIZE_DEBOUNCE) 68 | calculateHeadingPositions() { 69 | this.updateHeadingPositions(); 70 | } 71 | 72 | @action 73 | updateHeadingPositions() { 74 | // get the heading positions, so we know when to activate the TOC item on scroll 75 | const postElement = document.querySelector( 76 | `[data-post-id="${this.args.postID}"]` 77 | ); 78 | 79 | if (!postElement) { 80 | return; 81 | } 82 | 83 | const headings = postElement.querySelectorAll("h1, h2, h3, h4, h5"); 84 | if (!headings.length) { 85 | return; 86 | } 87 | 88 | const sameIdCount = new Map(); 89 | const mappedToc = this.mappedToc; 90 | this.headingPositions = Array.from(headings) 91 | .map((heading) => { 92 | const id = this.tocProcessor.getIdFromHeading( 93 | this.args.postID, 94 | heading, 95 | sameIdCount 96 | ); 97 | return mappedToc[id] 98 | ? { 99 | id, 100 | position: 101 | heading.getBoundingClientRect().top + 102 | window.scrollY - 103 | headerOffset() - 104 | POSITION_BUFFER, 105 | } 106 | : null; 107 | }) 108 | .compact(); 109 | } 110 | 111 | @debounce(SCROLL_DEBOUNCE) 112 | updateActiveHeadingOnScroll() { 113 | const scrollPosition = window.pageYOffset - headerOffset(); 114 | 115 | // binary search to find the active item 116 | let activeIndex = 0; 117 | let low = 0; 118 | let high = this.headingPositions.length - 1; 119 | while (low <= high) { 120 | let mid = Math.floor((low + high) / 2); 121 | let heading = this.headingPositions[mid]; 122 | 123 | if (scrollPosition >= heading.position) { 124 | low = mid + 1; 125 | activeIndex = mid; 126 | } else { 127 | high = mid - 1; 128 | } 129 | } 130 | 131 | const activeHeading = 132 | this.mappedToc[this.headingPositions[activeIndex]?.id]; 133 | 134 | this.activeHeadingId = activeHeading?.id; 135 | this.activeAncestorIds = []; 136 | let ancestor = activeHeading; 137 | while (ancestor && ancestor.parent) { 138 | this.activeAncestorIds.push(ancestor.parent.id); 139 | ancestor = ancestor.parent; 140 | } 141 | } 142 | 143 | mappedTocStructure(tocStructure, map = null) { 144 | map ??= {}; 145 | for (const item of tocStructure) { 146 | map[item.id] = item; 147 | if (item.subItems) { 148 | this.mappedTocStructure(item.subItems, map); 149 | } 150 | } 151 | return map; 152 | } 153 | 154 | 183 | } 184 | -------------------------------------------------------------------------------- /javascripts/discourse/components/toc-heading.gjs: -------------------------------------------------------------------------------- 1 | import Component from "@glimmer/component"; 2 | import { concat } from "@ember/helper"; 3 | import { on } from "@ember/modifier"; 4 | import { action } from "@ember/object"; 5 | import { service } from "@ember/service"; 6 | import { headerOffset } from "discourse/lib/offset-calculator"; 7 | import { slugify } from "discourse/lib/utilities"; 8 | 9 | const SCROLL_BUFFER = 25; 10 | 11 | export default class TocHeading extends Component { 12 | @service tocProcessor; 13 | 14 | get isActive() { 15 | return this.args.activeHeadingId === this.args.item.id; 16 | } 17 | 18 | get isAncestorActive() { 19 | return this.args.activeAncestorIds?.includes(this.args.item.id); 20 | } 21 | 22 | get classNames() { 23 | const baseClass = "d-toc-item"; 24 | const typeClass = this.args.item.tagName 25 | ? ` d-toc-${this.args.item.tagName}` 26 | : ""; 27 | let activeClass = ""; 28 | if (this.isActive) { 29 | activeClass = " direct-active active"; 30 | } else if (this.isAncestorActive) { 31 | activeClass = " active"; 32 | } 33 | return `${baseClass}${typeClass}${activeClass}`; 34 | } 35 | 36 | @action 37 | handleTocLinkClick(event) { 38 | event.preventDefault(); 39 | 40 | const targetId = event.target.href?.split("#").pop(); 41 | if (!targetId) { 42 | return; 43 | } 44 | 45 | const targetElement = 46 | document.querySelector(`a[name="${targetId}"]`) || 47 | document.getElementById(targetId); 48 | if (targetElement) { 49 | const headerOffsetValue = headerOffset(); 50 | const elementPosition = 51 | targetElement.getBoundingClientRect().top + window.pageYOffset; 52 | const offsetPosition = 53 | elementPosition - headerOffsetValue - SCROLL_BUFFER; 54 | 55 | window.scrollTo({ top: offsetPosition, behavior: "smooth" }); 56 | 57 | // hide TOC overlay when navigating to link 58 | this.tocProcessor.setOverlayVisible(false); 59 | } 60 | } 61 | 62 | 84 | } 85 | -------------------------------------------------------------------------------- /javascripts/discourse/components/toc-large-buttons.gjs: -------------------------------------------------------------------------------- 1 | import Component from "@glimmer/component"; 2 | import { action } from "@ember/object"; 3 | import { service } from "@ember/service"; 4 | import DButton from "discourse/components/d-button"; 5 | import { i18n } from "discourse-i18n"; 6 | 7 | export default class TocLargeButtons extends Component { 8 | @service tocProcessor; 9 | 10 | @action 11 | callJumpToEnd() { 12 | this.tocProcessor.jumpToEnd(this.args.renderTimeline, this.args.postID); 13 | } 14 | 15 | 25 | } 26 | -------------------------------------------------------------------------------- /javascripts/discourse/components/toc-mini-buttons.gjs: -------------------------------------------------------------------------------- 1 | import Component from "@glimmer/component"; 2 | import { action } from "@ember/object"; 3 | import { service } from "@ember/service"; 4 | import DButton from "discourse/components/d-button"; 5 | 6 | export default class TocMiniButtons extends Component { 7 | @service tocProcessor; 8 | 9 | @action 10 | callCloseOverlay() { 11 | this.tocProcessor.setOverlayVisible(false); 12 | } 13 | 14 | @action 15 | callJumpToEnd() { 16 | this.tocProcessor.jumpToEnd(this.args.renderTimeline, this.args.postID); 17 | } 18 | 19 | 33 | } 34 | -------------------------------------------------------------------------------- /javascripts/discourse/components/toc-mini.gjs: -------------------------------------------------------------------------------- 1 | import Component from "@glimmer/component"; 2 | import { action } from "@ember/object"; 3 | import { service } from "@ember/service"; 4 | import DButton from "discourse/components/d-button"; 5 | 6 | export default class TocMini extends Component { 7 | @service tocProcessor; 8 | 9 | willDestroy() { 10 | super.willDestroy(...arguments); 11 | this.removeClickOutsideListener(); 12 | } 13 | 14 | @action 15 | clickOutside() { 16 | this.tocProcessor.setOverlayVisible(false); 17 | this.removeClickOutsideListener(); 18 | } 19 | 20 | @action 21 | addClickOutsideListener() { 22 | document.addEventListener("click", this.clickOutside); 23 | } 24 | 25 | @action 26 | toggleTOCOverlay() { 27 | this.tocProcessor.toggleOverlay(); 28 | if (this.tocProcessor.isOverlayVisible) { 29 | this.addClickOutsideListener(); 30 | } else { 31 | this.removeClickOutsideListener(); 32 | } 33 | } 34 | 35 | @action 36 | removeClickOutsideListener() { 37 | document.removeEventListener("click", this.clickOutside); 38 | } 39 | 40 | 51 | } 52 | -------------------------------------------------------------------------------- /javascripts/discourse/components/toc-timeline.gjs: -------------------------------------------------------------------------------- 1 | import Component from "@glimmer/component"; 2 | import { tracked } from "@glimmer/tracking"; 3 | import { action } from "@ember/object"; 4 | import didInsert from "@ember/render-modifiers/modifiers/did-insert"; 5 | import didUpdate from "@ember/render-modifiers/modifiers/did-update"; 6 | import { service } from "@ember/service"; 7 | import bodyClass from "discourse/helpers/body-class"; 8 | import TocContents from "../components/toc-contents"; 9 | import TocToggle from "../components/toc-toggle"; 10 | 11 | export default class TocTimeline extends Component { 12 | @service tocProcessor; 13 | 14 | @tracked 15 | isTocVisible = localStorage.getItem("tocVisibility") === "true" || true; 16 | 17 | get shouldRenderToc() { 18 | if (!this.tocProcessor.hasTOC) { 19 | return false; 20 | } 21 | 22 | // should always show on docs routes 23 | if (this.tocProcessor.isDocs) { 24 | return true; 25 | } 26 | 27 | if (this.args.renderTimeline) { 28 | // single post topics might not have a timeline 29 | // so we should ignore state 30 | if (this.args.topic?.posts_count === 1) { 31 | return true; 32 | } 33 | 34 | // timeline state controlled by localStorage 35 | return this.tocProcessor.isTocVisible; 36 | } else { 37 | // progress state controlled by overlay state 38 | return this.tocProcessor.isOverlayVisible; 39 | } 40 | } 41 | 42 | get isTopicProgress() { 43 | return ( 44 | !this.args.renderTimeline || 45 | (this.args.renderTimeline && this.args.topicProgressExpanded) 46 | ); 47 | } 48 | 49 | @action 50 | callCheckPostforTOC() { 51 | this.tocProcessor.checkPostforTOC(this.args.topic); 52 | } 53 | 54 | @action 55 | handleTimelineUpdate() { 56 | if (this.args.renderTimeline) { 57 | this.tocProcessor.setOverlayVisible(false); 58 | } 59 | } 60 | 61 | 85 | } 86 | -------------------------------------------------------------------------------- /javascripts/discourse/components/toc-toggle.gjs: -------------------------------------------------------------------------------- 1 | import Component from "@glimmer/component"; 2 | import { service } from "@ember/service"; 3 | import DButton from "discourse/components/d-button"; 4 | import { i18n } from "discourse-i18n"; 5 | 6 | export default class TocToggle extends Component { 7 | @service tocProcessor; 8 | 9 | get shouldShow() { 10 | // docs and topics with 1 post don't need a toggle 11 | if (this.tocProcessor.isDocs || this.args.topic?.posts_count === 1) { 12 | return false; 13 | } 14 | 15 | return this.tocProcessor.hasTOC; 16 | } 17 | 18 | get toggleLabel() { 19 | return this.tocProcessor.isTocVisible 20 | ? "toggle_toc.show_timeline" 21 | : "toggle_toc.show_toc"; 22 | } 23 | 24 | get toggleIcon() { 25 | return this.tocProcessor.isTocVisible ? "timeline" : "bars-staggered"; 26 | } 27 | 28 | 38 | } 39 | -------------------------------------------------------------------------------- /javascripts/discourse/connectors/below-docs-topic/d-toc-wrapper.gjs: -------------------------------------------------------------------------------- 1 | import Component from "@ember/component"; 2 | import { classNames, tagName } from "@ember-decorators/component"; 3 | import TocTimeline from "../../components/toc-timeline"; 4 | 5 | @tagName("div") 6 | @classNames("below-docs-topic-outlet", "d-toc-wrapper") 7 | export default class DTocWrapper extends Component { 8 | 11 | } 12 | -------------------------------------------------------------------------------- /javascripts/discourse/connectors/topic-navigation/d-toc-wrapper.gjs: -------------------------------------------------------------------------------- 1 | import Component from "@ember/component"; 2 | import { classNames, tagName } from "@ember-decorators/component"; 3 | import TocTimeline from "../../components/toc-timeline"; 4 | 5 | @tagName("div") 6 | @classNames("topic-navigation-outlet", "d-toc-wrapper") 7 | export default class DTocWrapper extends Component { 8 | 15 | } 16 | -------------------------------------------------------------------------------- /javascripts/discourse/initializers/disco-toc-composer.js: -------------------------------------------------------------------------------- 1 | import { withPluginApi } from "discourse/lib/plugin-api"; 2 | import I18n from "discourse-i18n"; 3 | 4 | export default { 5 | name: "disco-toc-composer", 6 | 7 | initialize() { 8 | withPluginApi("1.0.0", (api) => { 9 | const currentUser = api.getCurrentUser(); 10 | if (!currentUser) { 11 | return; 12 | } 13 | 14 | const minimumTL = settings.minimum_trust_level_to_create_TOC; 15 | 16 | if (currentUser.trust_level >= minimumTL || currentUser.staff) { 17 | if (!I18n.translations[I18n.currentLocale()].js.composer) { 18 | I18n.translations[I18n.currentLocale()].js.composer = {}; 19 | } 20 | I18n.translations[I18n.currentLocale()].js.composer.contains_dtoc = " "; 21 | 22 | api.addComposerToolbarPopupMenuOption({ 23 | action: (toolbarEvent) => { 24 | toolbarEvent.applySurround( 25 | `
`, 26 | `
`, 27 | "contains_dtoc" 28 | ); 29 | }, 30 | icon: "align-left", 31 | label: themePrefix("insert_table_of_contents"), 32 | condition: (composer) => { 33 | return ( 34 | settings.enable_TOC_for_replies || composer.model.topicFirstPost 35 | ); 36 | }, 37 | }); 38 | 39 | if (settings.enable_TOC_for_replies) { 40 | document.body.classList.add("toc-for-replies-enabled"); 41 | } 42 | } 43 | }); 44 | }, 45 | }; 46 | -------------------------------------------------------------------------------- /javascripts/discourse/initializers/init-toc-mini.js: -------------------------------------------------------------------------------- 1 | import { apiInitializer } from "discourse/lib/api"; 2 | import TocMini from "../components/toc-mini"; 3 | 4 | export default apiInitializer("1.14.0", (api) => { 5 | api.renderInOutlet("before-topic-progress", TocMini); 6 | }); 7 | -------------------------------------------------------------------------------- /javascripts/discourse/initializers/init-toc-toggle.js: -------------------------------------------------------------------------------- 1 | import { apiInitializer } from "discourse/lib/api"; 2 | import TocToggle from "../components/toc-toggle"; 3 | 4 | export default apiInitializer("1.14.0", (api) => { 5 | api.renderInOutlet("timeline-footer-controls-after", TocToggle); 6 | }); 7 | -------------------------------------------------------------------------------- /javascripts/discourse/services/toc-processor.js: -------------------------------------------------------------------------------- 1 | import { tracked } from "@glimmer/tracking"; 2 | import { action } from "@ember/object"; 3 | import Service, { service } from "@ember/service"; 4 | import { slugify } from "discourse/lib/utilities"; 5 | 6 | export default class TocProcessor extends Service { 7 | @service router; 8 | 9 | @tracked hasTOC = false; 10 | @tracked postContent = null; 11 | @tracked postID = null; 12 | @tracked tocStructure = null; 13 | @tracked isTocVisible = localStorage.getItem("tocVisibility") !== "false"; 14 | @tracked isOverlayVisible = false; 15 | @tracked isDocs = false; 16 | 17 | @action 18 | toggleTocVisibility() { 19 | this.isTocVisible = !this.isTocVisible; 20 | localStorage.setItem("tocVisibility", this.isTocVisible); 21 | } 22 | 23 | setOverlayVisible(visible) { 24 | this.isOverlayVisible = visible; 25 | const tocWrapper = document.querySelector(".d-toc-wrapper"); 26 | if (tocWrapper) { 27 | tocWrapper.classList.toggle("overlay", visible); 28 | } 29 | } 30 | 31 | toggleOverlay() { 32 | this.setOverlayVisible(!this.isOverlayVisible); 33 | } 34 | 35 | checkPostforTOC(topic) { 36 | this.hasTOC = false; 37 | if ( 38 | this.isValidTopic(topic) && 39 | this.shouldDisplayToc(this.getCurrentPost(topic)) 40 | ) { 41 | const content = this.getCurrentPost(topic).cooked; 42 | if (this.containsTocMarkup(content) || this.autoTOC(topic)) { 43 | this.processPostContent(content, this.getCurrentPost(topic).id); 44 | } 45 | } 46 | this.setOverlayVisible(false); 47 | } 48 | 49 | isValidTopic(topic) { 50 | return !!topic; 51 | } 52 | 53 | getCurrentPost(topic) { 54 | const docs = this.router?.currentRouteName?.includes("docs"); 55 | 56 | if (docs) { 57 | this.isDocs = true; 58 | return topic.post_stream.posts[0]; 59 | } 60 | 61 | this.isDocs = false; 62 | return topic.postStream?.posts?.find( 63 | (post) => post.post_number === topic.currentPost 64 | ); 65 | } 66 | 67 | shouldDisplayToc(post) { 68 | return settings.enable_TOC_for_replies || post.post_number === 1; 69 | } 70 | 71 | containsTocMarkup(content) { 72 | return content.includes(`
`); 73 | } 74 | 75 | processPostContent(content, postId) { 76 | // no headings, no parsing 77 | if (this.containsHeadings(content)) { 78 | const parsedPost = new DOMParser().parseFromString(content, "text/html"); 79 | 80 | // direct descendants to avoid picking up headings in quotes 81 | const headings = parsedPost.querySelectorAll( 82 | "body > h1,body > h2,body > h3,body > h4,body > h5" 83 | ); 84 | 85 | if (headings.length < settings.TOC_min_heading) { 86 | this.setOverlayVisible(false); 87 | return; 88 | } 89 | 90 | this.populateTocData(postId, content, headings); 91 | } else { 92 | this.setOverlayVisible(false); 93 | } 94 | } 95 | 96 | containsHeadings(content) { 97 | return [" 98 | content.includes(tag) 99 | ); 100 | } 101 | 102 | populateTocData(postId, content, headings) { 103 | this.hasTOC = true; 104 | this.postID = postId; 105 | this.postContent = content; 106 | this.tocStructure = this.generateTocStructure(headings); 107 | } 108 | 109 | autoTOC(topic) { 110 | // check topic for categories or tags from settings 111 | const autoCategories = settings.auto_TOC_categories 112 | ? settings.auto_TOC_categories.split("|").map((id) => parseInt(id, 10)) 113 | : []; 114 | 115 | const autoTags = settings.auto_TOC_tags 116 | ? settings.auto_TOC_tags.split("|") 117 | : []; 118 | 119 | if ((!autoCategories.length && !autoTags.length) || !topic) { 120 | return false; 121 | } 122 | 123 | const topicCategory = topic.category_id; 124 | const topicTags = topic.tags || []; 125 | 126 | const hasMatchingTags = autoTags.some((tag) => topicTags.includes(tag)); 127 | const hasMatchingCategory = autoCategories.includes(topicCategory); 128 | 129 | // only apply autoTOC on first post 130 | // the docs plugin only shows the first post, and does not have topic.currentPost defined 131 | return ( 132 | (hasMatchingTags || hasMatchingCategory) && 133 | (topic.currentPost === 1 || topic.currentPost === undefined) 134 | ); 135 | } 136 | 137 | /** 138 | * @param {number} postId 139 | * @param {HTMLHeadingElement} heading 140 | * @param {Map} sameIdCount 141 | */ 142 | getIdFromHeading(postId, heading, sameIdCount) { 143 | const anchor = heading.querySelector("a.anchor"); 144 | if (anchor) { 145 | return anchor.name; 146 | } 147 | const lowerTagName = heading.tagName.toLowerCase(); 148 | const text = heading.textContent.trim(); 149 | let slug = `${slugify(text)}`; 150 | if (sameIdCount.has(slug)) { 151 | sameIdCount.set(slug, sameIdCount.get(slug) + 1); 152 | slug = `${slug}-${sameIdCount.get(slug)}`; 153 | } else { 154 | sameIdCount.set(slug, 1); 155 | } 156 | const res = `p-${postId}-toc-${lowerTagName}-${slug}`; 157 | heading.id = res; 158 | return res; 159 | } 160 | 161 | generateTocStructure(headings) { 162 | let root = { subItems: [], level: 0 }; 163 | let ancestors = [root]; 164 | 165 | const sameIdCount = new Map(); 166 | 167 | headings.forEach((heading) => { 168 | const level = parseInt(heading.tagName[1], 10); 169 | const text = heading.textContent.trim(); 170 | const lowerTagName = heading.tagName.toLowerCase(); 171 | 172 | const id = this.getIdFromHeading(this.postID, heading, sameIdCount); 173 | 174 | // Remove irrelevant ancestors 175 | while (ancestors[ancestors.length - 1].level >= level) { 176 | ancestors.pop(); 177 | } 178 | 179 | let headingData = { 180 | id, 181 | tagName: lowerTagName, 182 | text, 183 | subItems: [], 184 | level, 185 | parent: ancestors.length > 1 ? ancestors[ancestors.length - 1] : null, 186 | }; 187 | 188 | ancestors[ancestors.length - 1].subItems.push(headingData); 189 | ancestors.push(headingData); 190 | }); 191 | 192 | return root.subItems; 193 | } 194 | 195 | jumpToEnd(renderTimeline, postID) { 196 | let buffer = 150; 197 | const postContainer = document.querySelector(`[data-post-id="${postID}"]`); 198 | 199 | if (!renderTimeline) { 200 | this.setOverlayVisible(false); 201 | } 202 | 203 | if (postContainer) { 204 | // if the topic map is present, we don't want to scroll past it 205 | // so the post controls are still visible 206 | const topicMapHeight = 207 | postContainer.querySelector(`.topic-map`)?.offsetHeight || 0; 208 | 209 | if ( 210 | postContainer.parentElement?.nextElementSibling?.querySelector( 211 | "div[data-theme-toc]" 212 | ) 213 | ) { 214 | // but if the next post also has a toc, just jump to it 215 | buffer = 30 - topicMapHeight; 216 | } 217 | 218 | const offsetPosition = 219 | postContainer.getBoundingClientRect().bottom + 220 | window.scrollY - 221 | buffer - 222 | topicMapHeight; 223 | 224 | window.scrollTo({ top: offsetPosition, behavior: "smooth" }); 225 | } 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /locales/ar.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | ar: 8 | table_of_contents: جدول المحتويات 9 | insert_table_of_contents: إدراج جدول محتويات 10 | jump_bottom: الانتقال إلى النهاية 11 | toggle_toc: 12 | show_timeline: الخط الزمني 13 | show_toc: المحتويات 14 | theme_metadata: 15 | settings: 16 | minimum_trust_level_to_create_TOC: الحد الأدنى لمستوى الثقة الذي يجب أن يكون لدى المستخدم من أجل رؤية زر جدول المحتويات في أداة الإنشاء 17 | composer_toc_text: النص الذي يظهر في أعلى لوحة المعاينة في أداة الأنشاء للإشارة إلى أن الموضوع سيتضمن جدول محتويات 18 | auto_TOC_categories: تفعيل جدول المحتويات تلقائيًا في الموضوعات الموجودة في هذه الفئات 19 | auto_TOC_tags: تفعيل جدول المحتويات تلقائيًا في الموضوعات التي تحتوي على هذه الوسوم 20 | TOC_min_heading: الحد الأدنى لعدد العناوين في الموضوع ليتم عرض جدول المحتويات 21 | enable_TOC_for_replies: يسمح بجدول محتويات للردود. لا تتأثر جداول المحتويات للردود بإعدادات وسوم جدول المحتويات التلقائية وفئات جدول المحتويات التلقائية ويجب إدراجها يدويًا. 22 | -------------------------------------------------------------------------------- /locales/be.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | be: 8 | -------------------------------------------------------------------------------- /locales/bg.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | bg: 8 | -------------------------------------------------------------------------------- /locales/bs_BA.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | bs_BA: 8 | -------------------------------------------------------------------------------- /locales/ca.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | ca: 8 | -------------------------------------------------------------------------------- /locales/cs.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | cs: 8 | table_of_contents: obsah 9 | insert_table_of_contents: Vložit obsah 10 | jump_bottom: Přeskočit na konec 11 | toggle_toc: 12 | show_timeline: Časová osa 13 | show_toc: Obsah 14 | theme_metadata: 15 | settings: 16 | minimum_trust_level_to_create_TOC: Minimální úroveň důvěry, kterou musí mít uživatel, aby se v editoru zobrazilo tlačítko Obsah 17 | auto_TOC_categories: Automaticky povolit obsah pro témata v těchto kategoriích 18 | auto_TOC_tags: Automaticky povolit TOC u témat s těmito štítky 19 | -------------------------------------------------------------------------------- /locales/da.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | da: 8 | -------------------------------------------------------------------------------- /locales/de.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | de: 8 | table_of_contents: Inhaltsverzeichnis 9 | insert_table_of_contents: Inhaltsverzeichnis einfügen 10 | jump_bottom: Zum Ende springen 11 | toggle_toc: 12 | show_timeline: Zeitleiste 13 | show_toc: Inhalt 14 | theme_metadata: 15 | settings: 16 | minimum_trust_level_to_create_TOC: Die minimale Vertrauensstufe, die ein Benutzer haben muss, um die Schaltfläche „Inhaltsverzeichnis“ im Editor zu sehen 17 | composer_toc_text: Text, der oben im Vorschaufenster des Editors erscheint und darauf hinweist, dass das Thema ein Inhaltsverzeichnis haben wird 18 | auto_TOC_categories: Automatisches Aktivieren des Inhaltsverzeichnisses für Themen in diesen Kategorien 19 | auto_TOC_tags: Automatisches Aktivieren des Inhaltsverzeichnisses für Themen mit diesen Schlagwörtern 20 | TOC_min_heading: Mindestanzahl von Überschriften in einem Thema, damit das Inhaltsverzeichnis angezeigt wird 21 | enable_TOC_for_replies: Erlaube Inhaltsverzeichnisse für Antworten. Inhaltsverzeichnisse für Antworten werden von den Einstellungen auto TOC tags und auto TOC categories nicht beeinflusst, sondern müssen manuell eingefügt werden. 22 | -------------------------------------------------------------------------------- /locales/el.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | el: 8 | -------------------------------------------------------------------------------- /locales/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | table_of_contents: table of contents 3 | insert_table_of_contents: Insert table of contents 4 | jump_bottom: Jump to end 5 | toggle_toc: 6 | show_timeline: Timeline 7 | show_toc: Contents 8 | theme_metadata: 9 | settings: 10 | minimum_trust_level_to_create_TOC: The minimum trust level a user must have in order to see the TOC button in the composer 11 | composer_toc_text: Text that appears at the top of the preview pane of the composer to indicate the topic will have a table of contents 12 | auto_TOC_categories: Automatically enable TOC on topics in these categories 13 | auto_TOC_tags: Automatically enable TOC on topics with these tags 14 | TOC_min_heading: Minimum number of headings in a topic for the table of contents to be shown 15 | enable_TOC_for_replies: Allows TOC for replies. TOCs for replies are not affected by the auto TOC tags and auto TOC categories settings and must be inserted manually. 16 | -------------------------------------------------------------------------------- /locales/en_GB.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | en_GB: 8 | -------------------------------------------------------------------------------- /locales/es.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | es: 8 | table_of_contents: tabla de contenidos 9 | insert_table_of_contents: Insertar tabla de contenido 10 | jump_bottom: Saltar al final 11 | toggle_toc: 12 | show_timeline: Línea de tiempo 13 | show_toc: Contenido 14 | theme_metadata: 15 | settings: 16 | minimum_trust_level_to_create_TOC: El nivel de confianza mínimo que debe tener un usuario para ver el botón TOC en el compositor 17 | composer_toc_text: El texto que aparezca en la parte superior del panel de vista previa del compositor para indicar el tema tendrá una tabla de contenido 18 | auto_TOC_categories: Activar automáticamente TOC en temas de estas categorías 19 | auto_TOC_tags: Activar automáticamente TOC en temas con estas etiquetas 20 | TOC_min_heading: Número mínimo de encabezados en un tema para que se muestre la tabla de contenido 21 | enable_TOC_for_replies: Permite que TOC responda. Las TOC para las respuestas no se ven afectadas por los ajustes de las etiquetas de TOC automático y categorías de TOC automático y se deben insertar manualmente. 22 | -------------------------------------------------------------------------------- /locales/et.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | et: 8 | -------------------------------------------------------------------------------- /locales/fa_IR.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | fa_IR: 8 | table_of_contents: فهرست مطالب 9 | insert_table_of_contents: فهرست مطالب را وارد کنید 10 | jump_bottom: پرش به انتها 11 | -------------------------------------------------------------------------------- /locales/fi.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | fi: 8 | table_of_contents: sisällysluettelo 9 | insert_table_of_contents: Lisää sisällysluettelo 10 | jump_bottom: Siirry loppuun 11 | toggle_toc: 12 | show_timeline: Aikajana 13 | show_toc: Sisältö 14 | theme_metadata: 15 | settings: 16 | minimum_trust_level_to_create_TOC: Vähimmäisluottamustaso, joka käyttäjällä täytyy olla, jotta hän näkee sisällysluettelopainikkeen tekstieditorissa 17 | composer_toc_text: Teksti, joka näkyy tekstieditorin esikatseluruudun yläosassa ja ilmaisee, että ketjulla on sisällysluettelo 18 | auto_TOC_categories: Ota sisällysluettelo automaattisesti käyttöön näiden alueiden ketjuissa 19 | auto_TOC_tags: Ota sisällysluettelo automaattisesti käyttöön ketjuissa, joilla on näitä tunnisteita 20 | TOC_min_heading: Ketjun otsikoiden vähimmäismäärä, jotta sisällysluettelo näytetään 21 | enable_TOC_for_replies: Sallii sisällysluettelo vastauksissa. Automaattisten sisällysluettelotunnisteiden ja automaattisten sisällysluetteloalueiden asetukset eivät vaikuta vastausten sisällysluetteloihin, ja ne on lisättävä manuaalisesti. 22 | -------------------------------------------------------------------------------- /locales/fr.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | fr: 8 | table_of_contents: table des matières 9 | insert_table_of_contents: Insérer une table des matières 10 | jump_bottom: Accéder à la fin 11 | toggle_toc: 12 | show_timeline: Chronologie 13 | show_toc: Sommaire 14 | theme_metadata: 15 | settings: 16 | minimum_trust_level_to_create_TOC: Le niveau de confiance minimum qu'un utilisateur doit avoir pour voir le bouton de la table des matières dans le compositeur 17 | composer_toc_text: Le texte qui apparaît en haut du volet d'aperçu du compositeur pour indiquer que le sujet comportera une table des matières 18 | auto_TOC_categories: Activer automatiquement la table des matières sur les sujets de ces catégories 19 | auto_TOC_tags: Activer automatiquement la table des matières sur les sujets avec ces étiquettes 20 | TOC_min_heading: Nombre minimum de titres dans un sujet pour que la table des matières soit affichée 21 | enable_TOC_for_replies: Autorise la table des matières pour les réponses. Les tables des matières pour les réponses ne sont pas affectées par les paramètres des étiquettes de table des matières automatiques et des catégories de table des matières automatiques et doivent être insérées manuellement. 22 | -------------------------------------------------------------------------------- /locales/gl.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | gl: 8 | -------------------------------------------------------------------------------- /locales/he.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | he: 8 | table_of_contents: תוכן העניינים 9 | insert_table_of_contents: הוספת תוכן עניינים 10 | jump_bottom: דילוג לסוף 11 | toggle_toc: 12 | show_timeline: ציר זמן 13 | show_toc: תוכן 14 | theme_metadata: 15 | settings: 16 | minimum_trust_level_to_create_TOC: דרגת האמון המזערית שחייבת להיות למשתמש כדי לצפות בכפתור תוכן העניינים בחלונית הכתיבה 17 | composer_toc_text: טקסט שמופיע בחלק העליון של חלונית התצוגה המקדימה של עורך הפוסטים כדי לציין שהנושא יכלול תוכן עניינים 18 | auto_TOC_categories: להפעיל תוכן עניינים אוטומטית לנושאים בקטגוריות האלו 19 | auto_TOC_tags: להפעיל תוכן עניינים אוטומטית לנושאים עם התגיות האלו 20 | TOC_min_heading: מספר כותרות מזערי בנושא להצגת תוכן עניינים 21 | enable_TOC_for_replies: מאפשר תוכן עניינים לתגובות. תוכני עניינים לתגובות לא מושפעים מהגדרות תגיות תוכן העניינים (TOC) האוטומטיות וקטגוריות תוכן העניינים (TOC) האוטומטיות ויש להוסיף אותן ידנית. 22 | -------------------------------------------------------------------------------- /locales/hr.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | hr: 8 | -------------------------------------------------------------------------------- /locales/hu.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | hu: 8 | -------------------------------------------------------------------------------- /locales/hy.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | hy: 8 | -------------------------------------------------------------------------------- /locales/id.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | id: 8 | -------------------------------------------------------------------------------- /locales/it.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | it: 8 | table_of_contents: sommario 9 | insert_table_of_contents: Inserisci il sommario 10 | jump_bottom: Vai alla fine 11 | toggle_toc: 12 | show_timeline: Sequenza temporale 13 | show_toc: Contenuti 14 | theme_metadata: 15 | settings: 16 | minimum_trust_level_to_create_TOC: Il livello di attendibilità minimo che un utente deve avere per poter vedere il pulsante Sommario nel compositore 17 | composer_toc_text: Il testo che appare nella parte superiore del riquadro di anteprima del compositore per indicare che l'argomento avrà un sommario 18 | auto_TOC_categories: Abilita automaticamente il sommario sugli argomenti in queste categorie 19 | auto_TOC_tags: Abilita automaticamente il sommario sugli argomenti con queste etichette 20 | TOC_min_heading: Numero minimo di intestazioni in un argomento affinché il sommario venga mostrato 21 | enable_TOC_for_replies: Consente il sommario per le risposte. I sommari delle risposte non sono influenzati dalle impostazioni Etichette sommario automatiche e Categorie sommario automatico e devono essere inseriti manualmente. 22 | -------------------------------------------------------------------------------- /locales/ja.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | ja: 8 | table_of_contents: 目次 9 | insert_table_of_contents: 目次を挿入 10 | jump_bottom: 最後まで移動 11 | toggle_toc: 12 | show_timeline: タイムライン 13 | show_toc: コンテンツ 14 | theme_metadata: 15 | settings: 16 | minimum_trust_level_to_create_TOC: コンポーザーに TOC ボタンを表示するためにユーザーに必要な最低信頼レベル 17 | composer_toc_text: トピックに目次が挿入されることを示す、コンポーザーのプレビューペインの上部に表示されるテキスト 18 | auto_TOC_categories: これらのカテゴリのトピックで TOC を自動的に有効にする 19 | auto_TOC_tags: これらのタグのあるトピックで TOC を自動的に有効にする 20 | TOC_min_heading: 目次を表示するために必要なトピックの見出しの最低数 21 | enable_TOC_for_replies: 返信の TOC を許可します。返信の TOC は自動 TOC タグ自動 TOC カテゴリの設定の影響はなく、手動で挿入する必要があります。 22 | -------------------------------------------------------------------------------- /locales/ko.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | ko: 8 | -------------------------------------------------------------------------------- /locales/lt.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | lt: 8 | -------------------------------------------------------------------------------- /locales/lv.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | lv: 8 | -------------------------------------------------------------------------------- /locales/nb_NO.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | nb_NO: 8 | -------------------------------------------------------------------------------- /locales/nl.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | nl: 8 | table_of_contents: inhoudsopgave 9 | insert_table_of_contents: Inhoudsopgave invoegen 10 | jump_bottom: Naar einde 11 | toggle_toc: 12 | show_timeline: Tijdlijn 13 | show_toc: Inhoud 14 | theme_metadata: 15 | settings: 16 | minimum_trust_level_to_create_TOC: Het minimale vertrouwensniveau dat een gebruiker moet hebben om de inhoudsopgaveknop te zien in de editor 17 | composer_toc_text: Tekst die bovenaan het voorbeeldvenster van de editor wordt weergegeven om aan te geven dat het topic een inhoudsopgave heeft 18 | auto_TOC_categories: Inhoudsopgave automatisch inschakelen voor topics in deze categorieën 19 | auto_TOC_tags: Inhoudsopgave automatisch inschakelen voor topics met deze tags 20 | TOC_min_heading: Minimaal aantal koppen in een topic voordat de inhoudsopgave wordt weergegeven 21 | enable_TOC_for_replies: Staat een inhoudsopgave toe voor antwoorden. Inhoudsopgaven voor antwoorden worden niet beïnvloed door de instellingen auto TOC tags en auto TOC categories en moeten handmatig worden ingevoegd. 22 | -------------------------------------------------------------------------------- /locales/pl_PL.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | pl_PL: 8 | -------------------------------------------------------------------------------- /locales/pt.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | pt: 8 | -------------------------------------------------------------------------------- /locales/pt_BR.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | pt_BR: 8 | table_of_contents: tabela de conteúdo 9 | insert_table_of_contents: Inserir tabela de conteúdo 10 | jump_bottom: Ir para o fim 11 | toggle_toc: 12 | show_timeline: Linha de tempo 13 | show_toc: Conteúdo 14 | theme_metadata: 15 | settings: 16 | minimum_trust_level_to_create_TOC: O nível de confiança mínimo necessário para visualizar o botão TOC no compositor 17 | composer_toc_text: Texto exibido no topo do painel de pré-visualização do compositor para indicar que o tópico terá uma tabela de conteúdo 18 | auto_TOC_categories: Ativar automaticamente TOC em tópicos nestas categorias 19 | auto_TOC_tags: Ativar automaticamente TOC em tópicos com estas etiquetas 20 | TOC_min_heading: Quantidade mínima de cabeçalhos em um tópico para exibir a tabela de conteúdo 21 | enable_TOC_for_replies: Permitir TOC para respostas. TOCs para respostas não são afetados pelas configurações de etiquetas de TOC automáticas e categorias de TOC automáticas e devem ser inseridas manualmente. 22 | -------------------------------------------------------------------------------- /locales/ro.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | ro: 8 | table_of_contents: cuprins 9 | insert_table_of_contents: Introdu cuprins 10 | jump_bottom: Sari la final 11 | toggle_toc: 12 | show_timeline: Cronologie 13 | show_toc: Conținut 14 | theme_metadata: 15 | settings: 16 | minimum_trust_level_to_create_TOC: Nivelul minim de încredere pe care trebuie să îl aibă un utilizator pentru a vedea butonul TOC în compozitor 17 | auto_TOC_categories: Activează automat TOC pentru subiectele din aceste categorii 18 | auto_TOC_tags: Activează automat TOC în subiectele cu aceste etichete 19 | -------------------------------------------------------------------------------- /locales/ru.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | ru: 8 | table_of_contents: оглавление 9 | insert_table_of_contents: Вставить оглавление 10 | jump_bottom: Перейти к концу 11 | toggle_toc: 12 | show_timeline: Шкала времени 13 | show_toc: Оглавление 14 | theme_metadata: 15 | settings: 16 | minimum_trust_level_to_create_TOC: Минимальный уровень доверия, который должен иметь пользователь, чтобы увидеть кнопку оглавления в редакторе 17 | composer_toc_text: Текст, появляющийся в верхней части панели предварительного просмотра редактора и указывающий на то, что тема будет иметь оглавление 18 | auto_TOC_categories: Автоматически включает оглавление для тем в этих категориях 19 | auto_TOC_tags: Автоматически включать оглавление в темах с этими тегами 20 | TOC_min_heading: Минимальное количество заголовков в теме, чтобы отобразить оглавление 21 | enable_TOC_for_replies: Позволяет вставлять оглавление для ответов. На оглавления для ответов не влияют настройки auto TOC tags и auto TOC categories, и их необходимо вставлять вручную. 22 | -------------------------------------------------------------------------------- /locales/sk.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | sk: 8 | -------------------------------------------------------------------------------- /locales/sl.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | sl: 8 | -------------------------------------------------------------------------------- /locales/sq.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | sq: 8 | -------------------------------------------------------------------------------- /locales/sr.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | sr: 8 | -------------------------------------------------------------------------------- /locales/sv.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | sv: 8 | -------------------------------------------------------------------------------- /locales/sw.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | sw: 8 | -------------------------------------------------------------------------------- /locales/te.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | te: 8 | -------------------------------------------------------------------------------- /locales/th.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | th: 8 | -------------------------------------------------------------------------------- /locales/tr_TR.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | tr_TR: 8 | table_of_contents: içindekiler 9 | insert_table_of_contents: İçindekiler tablosu ekle 10 | jump_bottom: Sona atla 11 | toggle_toc: 12 | show_timeline: Zaman çizelgesi 13 | show_toc: İçindekiler 14 | theme_metadata: 15 | settings: 16 | minimum_trust_level_to_create_TOC: Bir kullanıcının bestecideki İçindekiler düğmesini görebilmesi için sahip olması gereken minimum güven seviyesi 17 | composer_toc_text: Bestecinin ön izleme bölmesinin en üstünde görünen ve konunun bir içerik tablosuna sahip olacağını belirten metin 18 | auto_TOC_categories: Bu kategorilerdeki konulardaki İçindekiler'i otomatik olarak etkinleştir 19 | auto_TOC_tags: Bu etiketlere sahip konulardaki İçindekiler'i otomatik olarak etkinleştir 20 | TOC_min_heading: İçindekiler tablosunun gösterilebilmesi için bir konudaki minimum başlık sayısı 21 | enable_TOC_for_replies: Yanıtlar için İçindekiler tablosuna izin verir. Yanıtlar için İçindekiler, otomatik İçindekiler etiketleri ve otomatik İçindekiler kategorileri ayarlarından etkilenmez ve manuel olarak eklenmelidir. 22 | -------------------------------------------------------------------------------- /locales/ug.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | ug: 8 | -------------------------------------------------------------------------------- /locales/uk.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | uk: 8 | -------------------------------------------------------------------------------- /locales/ur.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | ur: 8 | -------------------------------------------------------------------------------- /locales/vi.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | vi: 8 | -------------------------------------------------------------------------------- /locales/zh_CN.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | zh_CN: 8 | table_of_contents: 目录 9 | insert_table_of_contents: 插入目录 10 | jump_bottom: 跳至结尾 11 | toggle_toc: 12 | show_timeline: 时间线 13 | show_toc: 目录 14 | theme_metadata: 15 | settings: 16 | minimum_trust_level_to_create_TOC: 能够在编辑器中看到 TOC 按钮的最低信任级别 17 | composer_toc_text: 显示在编辑器预览窗格顶部的文本,用于指示该话题将有目录 18 | auto_TOC_categories: 在这些类别的话题中自动启用目录 19 | auto_TOC_tags: 在带有这些标签的话题中自动启用目录 20 | TOC_min_heading: 目录中显示的话题的最小标题数 21 | enable_TOC_for_replies: 允许使用目录进行回复。使用目录进行回复不受自动目录标签自动目录类别设置影响,必须手动插入。 22 | -------------------------------------------------------------------------------- /locales/zh_TW.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | zh_TW: 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "devDependencies": { 4 | "@discourse/lint-configs": "2.21.0", 5 | "ember-template-lint": "7.7.0", 6 | "eslint": "9.27.0", 7 | "prettier": "3.5.3", 8 | "stylelint": "16.19.1" 9 | }, 10 | "engines": { 11 | "node": ">= 22", 12 | "npm": "please-use-pnpm", 13 | "yarn": "please-use-pnpm", 14 | "pnpm": "9.x" 15 | }, 16 | "packageManager": "pnpm@9.15.5" 17 | } 18 | -------------------------------------------------------------------------------- /settings.yml: -------------------------------------------------------------------------------- 1 | minimum_trust_level_to_create_TOC: 2 | default: 0 3 | type: enum 4 | choices: 5 | - 1 6 | - 2 7 | - 3 8 | - 4 9 | composer_toc_text: 10 | default: "This topic will contain a table of contents" 11 | auto_TOC_categories: 12 | type: list 13 | list_type: category 14 | default: "" 15 | auto_TOC_tags: 16 | type: list 17 | list_type: tag 18 | default: "" 19 | enable_TOC_for_replies: 20 | default: false 21 | TOC_min_heading: 22 | default: 3 23 | min: 1 24 | max: 10000 25 | -------------------------------------------------------------------------------- /spec/system/core_features_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe "Core features", type: :system do 4 | before { upload_theme_or_component } 5 | 6 | it_behaves_like "having working core features" 7 | end 8 | -------------------------------------------------------------------------------- /spec/system/discotoc_author_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe "DiscoTOC", system: true do 4 | let!(:theme) { upload_theme_component } 5 | 6 | fab!(:category) 7 | fab!(:user) { Fabricate(:user, trust_level: TrustLevel[1], refresh_auto_groups: true) } 8 | 9 | fab!(:topic_1) { Fabricate(:topic) } 10 | fab!(:post_1) do 11 | Fabricate( 12 | :post, 13 | raw: 14 | "
\n\n# Heading 1\nContent for the first heading\n## Heading 2\nContent for the second heading\n### Heading 3\nContent for the third heading\n# Heading 4\nContent for the fourth heading", 15 | topic: topic_1, 16 | ) 17 | end 18 | 19 | before { sign_in(user) } 20 | 21 | it "composer has table of contents button" do 22 | visit("/c/#{category.id}") 23 | 24 | find("#create-topic").click 25 | find(".toolbar-popup-menu-options").click 26 | 27 | expect(page).to have_css("[data-name='Insert table of contents']") 28 | end 29 | 30 | it "table of contents button inserts markup into composer" do 31 | visit("/c/#{category.id}") 32 | 33 | find("#create-topic").click 34 | find(".toolbar-popup-menu-options").click 35 | find("[data-name='Insert table of contents']").click 36 | 37 | expect(page).to have_css(".d-editor-preview [data-theme-toc='true']") 38 | end 39 | 40 | it "table of contents button is hidden by trust level setting" do 41 | theme.update_setting(:minimum_trust_level_to_create_TOC, "2") 42 | theme.save! 43 | 44 | visit("/c/#{category.id}") 45 | 46 | find("#create-topic").click 47 | find(".toolbar-popup-menu-options").click 48 | 49 | expect(page).to have_no_css("[data-name='Insert table of contents']") 50 | end 51 | 52 | it "table of contents button does not appear on replies" do 53 | visit("/t/#{topic_1.id}") 54 | 55 | find(".reply").click 56 | find(".toolbar-popup-menu-options").click 57 | 58 | expect(page).to have_no_css("[data-name='Insert table of contents']") 59 | end 60 | 61 | context "when enable TOC for replies" do 62 | before do 63 | theme.update_setting(:enable_TOC_for_replies, true) 64 | theme.save! 65 | end 66 | 67 | it "table of contents button does appear on replies" do 68 | visit("/t/#{topic_1.id}") 69 | 70 | find(".reply").click 71 | find(".toolbar-popup-menu-options").click 72 | 73 | expect(page).to have_css("[data-name='Insert table of contents']") 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /spec/system/discotoc_progress_user_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe "DiscoTOC", system: true do 4 | let!(:theme) { upload_theme_component } 5 | 6 | fab!(:category) 7 | fab!(:tag) 8 | 9 | fab!(:topic_1) { Fabricate(:topic, category: category, tags: [tag]) } 10 | fab!(:topic_2) { Fabricate(:topic, category: category, tags: [tag]) } 11 | 12 | fab!(:post_1) do 13 | Fabricate( 14 | :post, 15 | raw: 16 | "
\n\n# Heading 1\nContent for the first heading\n## Heading 2\nContent for the second heading\n### Heading 3\nContent for the third heading\n# Heading 4\nContent for the fourth heading", 17 | topic: topic_1, 18 | ) 19 | end 20 | 21 | fab!(:post_2) do 22 | Fabricate( 23 | :post, 24 | raw: 25 | "\n# Heading 1\nContent for the first heading\n## Heading 2\nContent for the second heading\n### Heading 3\nContent for the third heading\n# Heading 4\nContent for the fourth heading", 26 | topic: topic_2, 27 | ) 28 | end 29 | 30 | fab!(:post_3) do 31 | Fabricate( 32 | :post, 33 | raw: 34 | "intentionally \n long \n content \n so \n there's \n plenty \n to be \n scrolled \n past \n which \n will \n force \n the \n timeline \n to \n hide \n scroll \n scroll \n scroll \n scroll \n scroll \n scroll \n scroll \n scroll \n scroll \n scroll \n scroll \n scroll \n scroll \n scroll \n scroll ", 35 | topic: topic_1, 36 | ) 37 | end 38 | 39 | fab!(:post_4) do 40 | Fabricate( 41 | :post, 42 | raw: 43 | "
\n\n# Heading For Reply 1\nContent for the first heading\n## Heading For Reply 2\nContent for the second heading\n### Heading For Reply 3\nContent for the third heading\n# Heading For Reply 4\nContent for the fourth heading", 44 | topic: topic_1, 45 | ) 46 | end 47 | 48 | fab!(:post_5) do 49 | Fabricate( 50 | :post, 51 | raw: 52 | "intentionally \n long \n content \n so \n there's \n plenty \n to be \n scrolled \n past \n which \n will \n force \n the \n timeline \n to \n hide \n scroll \n scroll \n scroll \n scroll \n scroll \n scroll \n scroll \n scroll \n scroll \n scroll \n scroll \n scroll \n scroll \n scroll \n scroll ", 53 | topic: topic_1, 54 | ) 55 | end 56 | 57 | it "table of contents button appears in mobile view" do 58 | visit("/t/#{topic_1.id}/?mobile_view=1") 59 | 60 | expect(page).to have_css(".d-toc-mini") 61 | end 62 | 63 | it "clicking the toggle button toggles the timeline" do 64 | visit("/t/#{topic_1.id}/?mobile_view=1") 65 | 66 | find(".d-toc-mini").click 67 | 68 | expect(page).to have_css(".d-toc-wrapper.overlay") 69 | end 70 | 71 | it "timeline toggle does not appear when the progress bar timeline is expanded" do 72 | visit("/t/#{topic_1.id}/?mobile_view=1") 73 | 74 | find("#topic-progress").click 75 | 76 | expect(page).to have_no_css(".timeline-fullscreen .timeline-toggle") 77 | end 78 | 79 | it "d-toc-mini is hidden when scrolled past the first post" do 80 | visit("/t/#{topic_1.id}/?mobile_view=1") 81 | 82 | page.execute_script <<~JS 83 | window.scrollTo(0, document.body.scrollHeight); 84 | JS 85 | 86 | expect(page).to have_no_css(".d-toc-mini") 87 | end 88 | 89 | it "d-toc-mini does not appear if the first post does not contain the markup" do 90 | visit("/t/#{topic_2.id}/?mobile_view=1") 91 | 92 | expect(page).to have_no_css(".d-toc-mini") 93 | end 94 | 95 | it "d-toc-mini will appear without markup if auto_TOC_categories is set to the topic's category" do 96 | theme.update_setting(:auto_TOC_categories, "#{category.id}") 97 | theme.save! 98 | 99 | visit("/t/#{topic_2.id}/?mobile_view=1") 100 | 101 | expect(page).to have_css(".d-toc-mini") 102 | end 103 | 104 | context "when disable TOC for replies" do 105 | before do 106 | theme.update_setting(:enable_TOC_for_replies, false) 107 | theme.save! 108 | end 109 | 110 | it "table of contents button won't appears in mobile view for replies" do 111 | visit("/t/-/#{topic_1.id}/3/?mobile_view=1") 112 | 113 | expect(page).to have_no_css(".d-toc-mini") 114 | end 115 | end 116 | 117 | context "when enable TOC for replies" do 118 | before do 119 | theme.update_setting(:enable_TOC_for_replies, true) 120 | theme.save! 121 | end 122 | 123 | it "table of contents button appears in mobile view for replies" do 124 | visit("/t/-/#{topic_1.id}/3/?mobile_view=1") 125 | 126 | expect(page).to have_css(".d-toc-mini") 127 | end 128 | 129 | it "d-toc-mini will not appear without markup for replies regardless of auto_TOC_categories and auto_TOC_tags" do 130 | theme.update_setting(:auto_TOC_categories, "#{category.id}") 131 | theme.update_setting(:auto_TOC_tags, "#{tag.name}") 132 | theme.save! 133 | 134 | visit("/t/-/#{topic_1.id}/2/?mobile_view=1") 135 | 136 | expect(page).to have_no_css(".d-toc-mini") 137 | end 138 | end 139 | 140 | it "d-toc-mini will not appear automatically if auto_TOC_categories is set to a different category" do 141 | theme.update_setting(:auto_TOC_categories, "99") 142 | theme.save! 143 | 144 | visit("/t/#{topic_2.id}/?mobile_view=1") 145 | 146 | expect(page).to have_no_css(".d-toc-mini") 147 | end 148 | 149 | it "d-toc-mini will appear without markup if auto_TOC_tags is set to the topic's tag" do 150 | theme.update_setting(:auto_TOC_tags, "#{tag.name}") 151 | theme.save! 152 | 153 | visit("/t/#{topic_2.id}/?mobile_view=1") 154 | 155 | expect(page).to have_css(".d-toc-mini") 156 | end 157 | 158 | it "d-toc-mini will not appear automatically if auto_TOC_tags is set to a different tag" do 159 | theme.update_setting(:auto_TOC_tags, "wrong-tag") 160 | theme.save! 161 | 162 | visit("/t/#{topic_2.id}/?mobile_view=1") 163 | 164 | expect(page).to have_no_css(".d-toc-mini") 165 | end 166 | 167 | it "d-toc-mini does not appear if it has fewer headings than TOC_min_heading setting" do 168 | theme.update_setting(:TOC_min_heading, 5) 169 | theme.save! 170 | 171 | visit("/t/#{topic_1.id}/?mobile_view=1") 172 | 173 | expect(page).to have_no_css(".d-toc-mini") 174 | end 175 | end 176 | -------------------------------------------------------------------------------- /spec/system/discotoc_timeline_user_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe "DiscoTOC", system: true do 4 | let!(:theme) { upload_theme_component } 5 | 6 | fab!(:category) 7 | fab!(:tag) 8 | 9 | fab!(:topic_1) { Fabricate(:topic, category: category, tags: [tag]) } 10 | fab!(:topic_2) { Fabricate(:topic, category: category, tags: [tag]) } 11 | 12 | fab!(:post_1) do 13 | Fabricate( 14 | :post, 15 | raw: 16 | "
\n\n# Heading 1\nContent for the first heading\n## Heading 2\nContent for the second heading\n### Heading 3\nContent for the third heading\n# Heading 4\nContent for the fourth heading", 17 | topic: topic_1, 18 | ) 19 | end 20 | 21 | fab!(:post_2) do 22 | Fabricate( 23 | :post, 24 | raw: 25 | "\n# Heading 1\nContent for the first heading\n## Heading 2\nContent for the second heading\n### Heading 3\nContent for the third heading\n# Heading 4\nContent for the fourth heading", 26 | topic: topic_2, 27 | ) 28 | end 29 | 30 | fab!(:post_3) do 31 | Fabricate( 32 | :post, 33 | raw: 34 | "intentionally \n long \n content \n so \n there's \n plenty \n to be \n scrolled \n past \n which \n will \n force \n the \n timeline \n to \n hide \n scroll \n scroll \n scroll \n scroll \n scroll \n scroll \n scroll \n scroll \n scroll \n scroll \n scroll \n scroll \n scroll \n scroll \n scroll ", 35 | topic: topic_1, 36 | ) 37 | end 38 | 39 | fab!(:post_4) do 40 | Fabricate( 41 | :post, 42 | raw: 43 | "
\n\n# Heading For Reply 1\nContent for the first heading\n## Heading For Reply 2\nContent for the second heading\n### Heading For Reply 3\nContent for the third heading\n# Heading For Reply 4\nContent for the fourth heading", 44 | topic: topic_1, 45 | ) 46 | end 47 | 48 | fab!(:post_5) do 49 | Fabricate( 50 | :post, 51 | raw: 52 | "intentionally \n long \n content \n so \n there's \n plenty \n to be \n scrolled \n past \n which \n will \n force \n the \n timeline \n to \n hide \n scroll \n scroll \n scroll \n scroll \n scroll \n scroll \n scroll \n scroll \n scroll \n scroll \n scroll \n scroll \n scroll \n scroll \n scroll ", 53 | topic: topic_1, 54 | ) 55 | end 56 | 57 | it "table of contents appears when the relevant markup is added to first post in topic" do 58 | visit("/t/#{topic_1.id}") 59 | 60 | expect(page).to have_css(".d-toc-item.d-toc-h1") 61 | end 62 | 63 | it "clicking the toggle button toggles the timeline" do 64 | visit("/t/#{topic_1.id}") 65 | 66 | find(".timeline-toggle").click 67 | 68 | expect(page).to have_css(".timeline-scrollarea-wrapper") 69 | 70 | find(".timeline-toggle").click 71 | 72 | expect(page).to have_css(".d-toc-item.d-toc-h1") 73 | end 74 | 75 | it "timeline does not appear when the table of contents is shown" do 76 | visit("/t/#{topic_1.id}") 77 | 78 | expect(page).to have_no_css(".topic-timeline") 79 | end 80 | 81 | it "table of contents is hidden when scrolled past the first post" do 82 | visit("/t/#{topic_1.id}") 83 | 84 | page.execute_script <<~JS 85 | window.scrollTo(0, document.body.scrollHeight); 86 | JS 87 | 88 | expect(page).to have_css(".topic-timeline") 89 | end 90 | 91 | it "table of contents does not appear if the first post does not contain the markup" do 92 | visit("/t/#{topic_2.id}") 93 | 94 | expect(page).to have_no_css(".d-toc-item.d-toc-h1") 95 | end 96 | 97 | it "table of contents updates the highlighted section after navigating directly to other topic" do 98 | source_topic = Fabricate(:topic, category: category, tags: [tag]) 99 | 100 | Fabricate( 101 | :post, 102 | topic: source_topic, 103 | raw: 104 | "
\n\n# Heading 1 on the source topic\nContent for the first heading\n## Heading 2\nContent for the second heading\n### Heading 3\nContent for the third heading\n# Heading 4\nContent for the fourth heading\nOther topic", 105 | ) 106 | visit("/t/#{source_topic.id}") 107 | 108 | expect(page).to have_css( 109 | ".d-toc-item.d-toc-h1.active a[data-d-toc='toc-h1-heading-1-on-the-source-topic']", 110 | ) 111 | 112 | find("a[href='/t/#{topic_1.slug}/#{topic_1.id}']").click 113 | 114 | expect(page).to have_css(".d-toc-item.d-toc-h1.active a[data-d-toc='toc-h1-heading-1']") 115 | expect(page).to have_no_css("a[data-d-toc='toc-h1-heading-1-on-the-source-topic']") 116 | end 117 | 118 | it "timeline will appear without markup if auto_TOC_categories is set to the topic's category" do 119 | theme.update_setting(:auto_TOC_categories, "#{category.id}") 120 | theme.save! 121 | 122 | visit("/t/#{topic_2.id}") 123 | 124 | expect(page).to have_css(".d-toc-item.d-toc-h1") 125 | end 126 | 127 | it "timeline will not appear automatically if auto_TOC_categories is set to a different category" do 128 | theme.update_setting(:auto_TOC_categories, "99") 129 | theme.save! 130 | 131 | visit("/t/#{topic_2.id}") 132 | 133 | expect(page).to have_no_css(".d-toc-item.d-toc-h1") 134 | end 135 | 136 | it "timeline will appear without markup if auto_TOC_tags is set to the topic's tag" do 137 | theme.update_setting(:auto_TOC_tags, "#{tag.name}") 138 | theme.save! 139 | 140 | visit("/t/#{topic_2.id}") 141 | 142 | expect(page).to have_css(".d-toc-item.d-toc-h1") 143 | end 144 | 145 | it "timeline will not appear automatically if auto_TOC_tags is set to a different tag" do 146 | theme.update_setting(:auto_TOC_tags, "wrong-tag") 147 | theme.save! 148 | 149 | visit("/t/#{topic_2.id}") 150 | 151 | expect(page).to have_no_css(".d-toc-item.d-toc-h1") 152 | end 153 | 154 | it "timeline does not appear if it has fewer headings than TOC_min_heading setting" do 155 | theme.update_setting(:TOC_min_heading, 5) 156 | theme.save! 157 | 158 | visit("/t/#{topic_1.id}") 159 | 160 | expect(page).to have_no_css(".d-toc-item.d-toc-h1") 161 | end 162 | 163 | context "when enable TOC for replies" do 164 | before do 165 | theme.update_setting(:enable_TOC_for_replies, true) 166 | theme.save! 167 | end 168 | 169 | it "timeline does not appear for replies when the table of contents is shown" do 170 | visit("/t/-/#{topic_1.id}/3") 171 | 172 | expect(page).to have_no_css(".topic-timeline") 173 | end 174 | 175 | it "d-toc-mini will not appear without markup for replies regardless of auto_TOC_categories and auto_TOC_tags" do 176 | theme.update_setting(:auto_TOC_categories, "#{category.id}") 177 | theme.update_setting(:auto_TOC_tags, "#{tag.name}") 178 | theme.save! 179 | 180 | visit("/t/-/#{topic_1.id}/2") 181 | 182 | expect(page).to have_no_css(".d-toc-item.d-toc-h1") 183 | end 184 | end 185 | end 186 | -------------------------------------------------------------------------------- /stylelint.config.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | extends: ["@discourse/lint-configs/stylelint"], 3 | }; 4 | -------------------------------------------------------------------------------- /translator.yml: -------------------------------------------------------------------------------- 1 | # Configuration file for discourse-translator-bot 2 | 3 | files: 4 | - source_path: locales/en.yml 5 | destination_path: translations.yml 6 | --------------------------------------------------------------------------------