rawfeed 0.1.4 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +20 -21
  3. data/README.md +12 -130
  4. data/_data/options.yml +270 -0
  5. data/_data/resume.yml +8 -8
  6. data/_includes/alert +3 -1
  7. data/_includes/chart +13 -32
  8. data/_includes/details +1 -57
  9. data/_includes/image +12 -4
  10. data/_includes/layout/blog_search.html +6 -4
  11. data/_includes/layout/data.liquid +21 -3
  12. data/_includes/layout/disqus.html +12 -26
  13. data/_includes/layout/footer.html +33 -17
  14. data/_includes/layout/giscus.html +27 -19
  15. data/_includes/layout/head.html +41 -41
  16. data/_includes/layout/header.html +127 -101
  17. data/_includes/layout/maintenance.html +6 -10
  18. data/_includes/layout/paginator.html +6 -4
  19. data/_includes/socials +7 -5
  20. data/_includes/tabs +1 -94
  21. data/_includes/toc +11 -194
  22. data/_includes/video +4 -1
  23. data/_layouts/blog.html +8 -7
  24. data/_layouts/contact.html +90 -196
  25. data/_layouts/default.html +42 -341
  26. data/_layouts/error.html +6 -4
  27. data/_layouts/home.html +45 -36
  28. data/_layouts/licenses.html +10 -0
  29. data/_layouts/page.html +4 -4
  30. data/_layouts/pixel.html +48 -0
  31. data/_layouts/pixels.html +71 -1
  32. data/_layouts/post.html +28 -29
  33. data/_layouts/resume.html +41 -34
  34. data/_layouts/tag.html +14 -3
  35. data/_layouts/tag_posts.html +3 -3
  36. data/_sass/base/_index.scss +39 -3
  37. data/_sass/components/_badges.scss +10 -0
  38. data/_sass/components/_markdown.scss +8 -5
  39. data/_sass/includes/_footer.scss +5 -2
  40. data/_sass/includes/_header.scss +23 -19
  41. data/_sass/includes/_highlight.scss +20 -7
  42. data/_sass/includes/_maintenance.scss +2 -3
  43. data/_sass/includes/_terminal.scss +35 -12
  44. data/_sass/layouts/_blog.scss +13 -9
  45. data/_sass/layouts/_contact.scss +6 -5
  46. data/_sass/layouts/_default.scss +5 -5
  47. data/_sass/layouts/_index.scss +3 -0
  48. data/_sass/layouts/_licenses.scss +7 -0
  49. data/_sass/layouts/_page.scss +1 -0
  50. data/_sass/layouts/_pixel.scss +61 -0
  51. data/_sass/layouts/_pixels.scss +86 -0
  52. data/_sass/layouts/_post.scss +4 -11
  53. data/_sass/layouts/_resume.scss +16 -3
  54. data/_sass/layouts/_tag-posts.scss +1 -2
  55. data/_sass/layouts/_tag.scss +12 -1
  56. data/_sass/main.scss +16 -1
  57. data/_sass/theme/_dark.scss +8 -1
  58. data/_sass/theme/_light.scss +8 -1
  59. data/assets/images/blog/.keep +0 -0
  60. data/assets/images/pixels/luffy.jpg +0 -0
  61. data/assets/js/blog.coffee +102 -0
  62. data/assets/js/contact.coffee +105 -0
  63. data/assets/js/default.coffee +172 -0
  64. data/assets/js/discus.coffee +30 -0
  65. data/assets/js/fallback/README.md +3 -0
  66. data/assets/js/fallback/blog.js +113 -0
  67. data/assets/js/fallback/contact.js +116 -0
  68. data/assets/js/{default.js → fallback/default.js} +50 -0
  69. data/assets/js/fallback/discus.js +32 -0
  70. data/{_includes/layout/google_analytics.html → assets/js/fallback/google_analytics.js} +7 -3
  71. data/assets/js/fallback/home.js +275 -0
  72. data/assets/js/fallback/no_inframe.js +4 -0
  73. data/assets/js/fallback/page.js +423 -0
  74. data/assets/js/fallback/pixels.js +1 -0
  75. data/assets/js/fallback/resume.js +13 -0
  76. data/assets/js/fallback/tags.js +1 -0
  77. data/{_includes/layout/capture_scripts.liquid → assets/js/fallback/theme_load.js} +0 -2
  78. data/assets/js/google_analytics.coffee +24 -0
  79. data/assets/js/home.coffee +250 -0
  80. data/assets/js/no_inframe.coffee +9 -0
  81. data/assets/js/page.coffee +379 -0
  82. data/assets/js/pixels.coffee +2 -0
  83. data/assets/js/resume.coffee +9 -0
  84. data/assets/js/tags.coffee +2 -0
  85. data/assets/js/theme_load.coffee +6 -0
  86. data/assets/json/blog_search.json +2 -2
  87. data/lib/rawfeed/author.rb +59 -0
  88. data/lib/rawfeed/csp_filters.rb +3 -0
  89. data/lib/rawfeed/draft.rb +1 -1
  90. data/lib/rawfeed/layout.rb +7 -0
  91. data/lib/rawfeed/page.rb +2 -2
  92. data/lib/rawfeed/pixel.rb +32 -0
  93. data/lib/rawfeed/post.rb +2 -2
  94. data/lib/rawfeed/resume.rb +1 -0
  95. data/lib/rawfeed/typescript_liquid.rb +172 -0
  96. data/lib/rawfeed/utils.rb +1 -0
  97. data/lib/rawfeed/version.rb +1 -1
  98. data/lib/rawfeed/with_class.rb +20 -0
  99. data/lib/rawfeed.rb +5 -1
  100. metadata +44 -12
  101. data/assets/js/avatar.js +0 -59
  102. data/assets/js/terminal.js +0 -18
@@ -0,0 +1,275 @@
1
+ ---
2
+ ---
3
+
4
+ {%- include layout/data.liquid -%}
5
+
6
+ document.addEventListener("DOMContentLoaded", () => {
7
+
8
+ const terminal = document.getElementById("terminal");
9
+
10
+ if (terminal) {
11
+
12
+ // effects terminal: maximize
13
+ // ----------------------------------------------------------------------------------------------
14
+ const btnMax = terminal.querySelector(".terminal-header__max");
15
+
16
+ let isFullscreen = false;
17
+
18
+ // maximize/restore
19
+ btnMax.addEventListener("click", () => {
20
+ isFullscreen = !isFullscreen;
21
+ terminal.classList.toggle("terminal-fullscreen", isFullscreen);
22
+ });
23
+
24
+ // populate the terminal
25
+ // ----------------------------------------------------------------------------------------------
26
+ const socialsEl = document.getElementById("terminal-screen--socials");
27
+ const screen = document.getElementById("screen");
28
+
29
+ const commands = {
30
+ // Multiple string is the same as using ` in Javascript
31
+ help: `{{ home_.terminal.help.menu }}`,
32
+ about: document.getElementById("home-content").innerHTML,
33
+ socials: socialsEl ? socialsEl.innerHTML : "{{ home_.terminal.no_socials }}"
34
+ };
35
+
36
+ const createInputLine = () => {
37
+ const line = document.createElement("div");
38
+ line.className = "line";
39
+
40
+ const prompt = document.createElement("span");
41
+ prompt.className = "prompt";
42
+ prompt.textContent = "[{{ home_.terminal.user }}@{{ home_.terminal.hostname }} ~]$";
43
+
44
+ // wrapper para conter input, cursor e measure
45
+ const wrapper = document.createElement("span");
46
+ wrapper.className = "input-wrapper";
47
+
48
+ const input = document.createElement("input");
49
+ input.type = "text";
50
+ input.className = "input";
51
+ input.placeholder = `{{ home_.terminal.welcome }}`;
52
+ input.spellcheck = false;
53
+ input.autocomplete = "off";
54
+ input.autocorrect = "off";
55
+ input.autocapitalize = "off";
56
+
57
+ const cursor = document.createElement("span");
58
+ cursor.className = "cursor";
59
+
60
+ const measure = document.createElement("span");
61
+ measure.className = "measure";
62
+
63
+ wrapper.appendChild(input);
64
+ wrapper.appendChild(cursor);
65
+ wrapper.appendChild(measure);
66
+
67
+ line.appendChild(prompt);
68
+ line.appendChild(wrapper);
69
+ screen.appendChild(line);
70
+
71
+ input.focus();
72
+ screen.scrollTop = screen.scrollHeight;
73
+
74
+ // Updates the fake cursor position based on the input"s selectionStart
75
+ const updateCursor = () => {
76
+ const sel = input.selectionStart || 0;
77
+ // measure the text to the position of the caret
78
+ measure.textContent = input.value.slice(0, sel);
79
+ const textWidth = measure.offsetWidth; // largura do texto sem scroll
80
+ const visibleLeft = textWidth - input.scrollLeft;
81
+ cursor.style.left = `${visibleLeft}px`;
82
+
83
+ // ensure the caret is visible (for long texts): adjust input"s scrollLeft
84
+ const paddingRight = 10;
85
+ if (textWidth - input.scrollLeft > input.clientWidth - paddingRight) {
86
+ input.scrollLeft = textWidth - input.clientWidth + paddingRight;
87
+ cursor.style.left = `${textWidth - input.scrollLeft}px`;
88
+ } else if (textWidth < input.scrollLeft) {
89
+ input.scrollLeft = textWidth;
90
+ cursor.style.left = `${textWidth - input.scrollLeft}px`;
91
+ }
92
+ };
93
+
94
+ // show/hide cursor animation as focus changes
95
+ const onFocus = () => {
96
+ cursor.style.opacity = "1";
97
+ updateCursor();
98
+ };
99
+
100
+ const onBlur = () => {
101
+ cursor.style.opacity = "0";
102
+ };
103
+
104
+ input.addEventListener("input", updateCursor);
105
+
106
+ input.addEventListener("keydown", (e) => {
107
+ // Update position on keys that do not trigger input immediately (arrows, delete, etc.)
108
+ setTimeout(updateCursor, 0);
109
+
110
+ if (e.key === "Enter") {
111
+ e.preventDefault();
112
+ const cmd = input.value.trim().toLowerCase();
113
+ if (cmd) {
114
+ // remove input/cursor/measure and place fixed text
115
+ wrapper.removeChild(input);
116
+ wrapper.removeChild(cursor);
117
+ wrapper.removeChild(measure);
118
+ const cmdText = document.createElement("span");
119
+ cmdText.textContent = cmd;
120
+ wrapper.appendChild(cmdText);
121
+ processCommand(cmd);
122
+ } else {
123
+ // if you enter without command, it just creates a new empty line (with prompt)
124
+ wrapper.removeChild(input);
125
+ wrapper.removeChild(cursor);
126
+ wrapper.removeChild(measure);
127
+ const blank = document.createElement("span");
128
+ blank.textContent = "";
129
+ wrapper.appendChild(blank);
130
+ }
131
+
132
+ // New line input
133
+ createInputLine();
134
+
135
+ } else if (e.key === "Escape") {
136
+ e.preventDefault();
137
+ screen.innerHTML = "";
138
+ createInputLine();
139
+ }
140
+ });
141
+
142
+ // arrows, mouse click, mouseup (position caret), etc.
143
+ input.addEventListener("keyup", updateCursor);
144
+ input.addEventListener("click", () => setTimeout(updateCursor, 0));
145
+ input.addEventListener("mouseup", () => setTimeout(updateCursor, 0));
146
+ input.addEventListener("focus", onFocus);
147
+ input.addEventListener("blur", onBlur);
148
+
149
+ updateCursor();
150
+ };
151
+
152
+ // function show date
153
+ function dateShow() {
154
+ const langBrowser = navigator.language || navigator.userLanguage;
155
+
156
+ const options = {
157
+ weekday: 'long',
158
+ year: 'numeric',
159
+ month: 'long',
160
+ day: 'numeric',
161
+ hour: '2-digit',
162
+ minute: '2-digit'
163
+ };
164
+
165
+ let brMessage = "";
166
+
167
+ if (langBrowser === "pt-BR" ) {
168
+ brMessage = " (Horário de Brasília)";
169
+ }
170
+
171
+ try {
172
+ return new Date().toLocaleString(langBrowser, options) + brMessage;
173
+ } catch (e) {
174
+ // Fallback for en-US
175
+ return new Date().toLocaleString('en-US', options) + brMessage;
176
+ }
177
+ }
178
+
179
+ // processes commands
180
+ const processCommand = (cmd) => {
181
+ if (cmd === "help") {
182
+ helpWriteHTML(commands.help, "html");
183
+ } else if (cmd === "date") {
184
+ commandsPrint(dateShow(), "text");
185
+ // else if (cmd.startsWith("echo "))
186
+ // commandsPrint(cmd.split(" ").slice(1).join(" "))
187
+ } else if (cmd === "about") {
188
+ aboutWriteHTML(commands.about);
189
+ } else if (cmd === "socials") {
190
+ socialsWriteHTML(commands.socials);
191
+ } else if (cmd === "clear") {
192
+ screen.innerHTML = "";
193
+ } else if (cmd) {
194
+ commandsPrint(cmd + `{{ home_.terminal.error }}`, "text");
195
+ }
196
+ };
197
+
198
+ const helpWriteHTML = (text, mode = "html") => {
199
+ const wrapper = document.createElement("div");
200
+ wrapper.className = "line-wrapper";
201
+ const helpDescription = document.createElement("span");
202
+ helpDescription.className = "help-description";
203
+ helpDescription.innerHTML = `{{ home_.terminal.help.description | markdownify }}`;
204
+ const helpCommandTitle = document.createElement("span");
205
+ helpCommandTitle.className = "help-commands__title";
206
+ helpCommandTitle.textContent = "{{ home_.terminal.help.commands.title }}";
207
+ const helpCommandDesc = document.createElement("span");
208
+ helpCommandDesc.className = "help-commands__desc";
209
+ helpCommandDesc.textContent = `{{ home_.terminal.help.commands.description }}`;
210
+
211
+ wrapper.appendChild(helpDescription);
212
+ wrapper.appendChild(helpCommandTitle);
213
+ wrapper.appendChild(helpCommandDesc);
214
+
215
+ text.split("\n").forEach((t) => {
216
+ const line = document.createElement("div");
217
+ line.className = "line";
218
+ if (mode === "html") line.innerHTML = t; else line.textContent = t;
219
+ wrapper.appendChild(line);
220
+ });
221
+
222
+ screen.appendChild(wrapper);
223
+ screen.scrollTop = screen.scrollHeight;
224
+ };
225
+
226
+ const socialsWriteHTML = (content, mode = "html") => {
227
+ const wrapper = document.createElement("div");
228
+ wrapper.className = "line-wrapper";
229
+ const socialsCommandDesc = document.createElement("span");
230
+ socialsCommandDesc.className = "socials-command__desc";
231
+ socialsCommandDesc.textContent = `{{ socials_.description.terminal.command.description }}`;
232
+
233
+ screen.appendChild(socialsCommandDesc);
234
+
235
+ if (mode === "html") wrapper.innerHTML = content; else wrapper.textContent = content;
236
+
237
+ screen.appendChild(wrapper);
238
+ screen.scrollTop = screen.scrollHeight;
239
+ };
240
+
241
+ const aboutWriteHTML = (content, mode = "html") => {
242
+ const wrapper = document.createElement("div");
243
+ wrapper.className = "line-wrapper";
244
+ if (mode === "html") wrapper.innerHTML = content; else wrapper.textContent = content;
245
+ screen.appendChild(wrapper);
246
+ screen.scrollTop = screen.scrollHeight;
247
+ };
248
+
249
+ const commandsPrint = (text, mode = "html") => {
250
+ // creates the wrapper to group all the lines
251
+ const wrapper = document.createElement("div");
252
+ wrapper.className = "line-wrapper";
253
+
254
+ text.split("\n").forEach((t) => {
255
+ const line = document.createElement("div");
256
+ line.className = "line";
257
+ if (mode === "html") line.innerHTML = t; else line.textContent = t;
258
+ wrapper.appendChild(line);
259
+ });
260
+
261
+ screen.appendChild(wrapper);
262
+ screen.scrollTop = screen.scrollHeight;
263
+ };
264
+
265
+ // start terminal
266
+ createInputLine();
267
+
268
+ // when clicking on the terminal, it always focuses on the last existing input
269
+ terminal.addEventListener("click", (e) => {
270
+ // avoids focusing when clicking a header button, etc.
271
+ const lastInput = screen.querySelector(".input:last-of-type");
272
+ if (lastInput) lastInput.focus();
273
+ });
274
+ }
275
+ });
@@ -0,0 +1,4 @@
1
+ if (window.top !== window.self) {
2
+ // Prevents the site from being displayed inside an <iframe>
3
+ window.top.location = window.self.location;
4
+ }
@@ -0,0 +1,423 @@
1
+ ---
2
+ ---
3
+
4
+
5
+ document.addEventListener("DOMContentLoaded", () => {
6
+
7
+ /* details
8
+ # ------------------------------------------------------------------------------------------------
9
+ */
10
+ const detailsStart = document.getElementById("details-start");
11
+
12
+ if (detailsStart) {
13
+ if (window.__jekyll_details_setup) return;
14
+ window.__jekyll_details_setup = true;
15
+
16
+ function initDetails(){
17
+ const starts = document.querySelectorAll('.details-start');
18
+ starts.forEach(start => {
19
+ const summary = start.getAttribute('data-summary') || 'Detalhes';
20
+
21
+ let end = start.nextSibling;
22
+ while(end && !(end.nodeType === 1 && end.classList.contains('details-end'))){
23
+ end = end.nextSibling;
24
+ }
25
+ if(!end) return;
26
+
27
+ let node = start.nextSibling;
28
+ const content = [];
29
+ while(node && node !== end){
30
+ const next = node.nextSibling;
31
+ if(node.nodeType === Node.ELEMENT_NODE || (node.nodeType === Node.TEXT_NODE && node.textContent.trim())){
32
+ content.push(node.cloneNode(true));
33
+ }
34
+ node = next;
35
+ }
36
+
37
+ const details = document.createElement('details');
38
+ const sum = document.createElement('summary');
39
+ sum.textContent = summary;
40
+ details.appendChild(sum);
41
+
42
+ const wrapper = document.createElement('div');
43
+ wrapper.className = 'details-content-wrapper';
44
+
45
+ content.forEach(el => wrapper.appendChild(el));
46
+
47
+ details.appendChild(wrapper);
48
+
49
+ start.parentNode.insertBefore(details, start);
50
+ let cur = start;
51
+ while(cur){
52
+ const next = cur.nextSibling;
53
+ cur.remove();
54
+ if(cur === end) break;
55
+ cur = next;
56
+ }
57
+ });
58
+ }
59
+
60
+ if(document.readyState === 'loading')
61
+ document.addEventListener('DOMContentLoaded', initDetails);
62
+ else
63
+ initDetails();
64
+ }
65
+
66
+ /* tabs
67
+ # ------------------------------------------------------------------------------------------------
68
+ */
69
+ const tabsStart = document.getElementById("tabs-start");
70
+
71
+ if (tabsStart) {
72
+ if (window.__simple_tabs_installed) return;
73
+ window.__simple_tabs_installed = true;
74
+
75
+ function processTabs() {
76
+ var starts = Array.from(document.querySelectorAll('.tabs-start'));
77
+ starts.forEach(function (start) {
78
+ var end = start.nextSibling;
79
+ while (end && !(end.nodeType === 1 && end.classList && end.classList.contains('tabs-end'))) {
80
+ end = end.nextSibling;
81
+ }
82
+ if (!end) return;
83
+
84
+ var node = start.nextSibling;
85
+ var tabs = [];
86
+ var currentTab = null;
87
+ while (node && node !== end) {
88
+ var next = node.nextSibling;
89
+ if (node.nodeType === Node.TEXT_NODE && !node.textContent.trim()) {
90
+ node = next; continue;
91
+ }
92
+ var text = (node.textContent || '').trim();
93
+ var m = text.match(/^\s*tab\d*\s*:\s*(.+)$/i);
94
+ if (m) {
95
+ currentTab = { title: m[1].trim(), nodes: [] };
96
+ tabs.push(currentTab);
97
+ if (node.parentNode) node.parentNode.removeChild(node);
98
+ } else if (currentTab) {
99
+ currentTab.nodes.push(node);
100
+ } else {
101
+ }
102
+ node = next;
103
+ }
104
+
105
+ if (tabs.length === 0) {
106
+ return;
107
+ }
108
+
109
+ var wrap = document.createElement('div');
110
+ wrap.className = 'tabs-wrap';
111
+
112
+ var nav = document.createElement('div');
113
+ nav.className = 'tabs-nav';
114
+
115
+ var panels = document.createElement('div');
116
+ panels.className = 'tabs-panels';
117
+
118
+ tabs.forEach(function (tab, i) {
119
+ var btn = document.createElement('button');
120
+ btn.type = 'button';
121
+ btn.className = 'tab-btn' + (i === 0 ? ' active' : '');
122
+ btn.setAttribute('data-idx', i);
123
+ btn.textContent = tab.title;
124
+ btn.addEventListener('click', function () {
125
+ var idx = +this.getAttribute('data-idx');
126
+ wrap.querySelectorAll('.tab-btn').forEach(function (b) {
127
+ b.classList.toggle('active', +b.getAttribute('data-idx') === idx);
128
+ });
129
+ wrap.querySelectorAll('.tab-panel').forEach(function (p, pi) {
130
+ p.classList.toggle('active', pi === idx);
131
+ });
132
+ });
133
+ nav.appendChild(btn);
134
+
135
+ var panel = document.createElement('div');
136
+ panel.className = 'tab-panel' + (i === 0 ? ' active' : '');
137
+ tab.nodes.forEach(function (n) {
138
+ panel.appendChild(n.cloneNode(true));
139
+ });
140
+ panels.appendChild(panel);
141
+ });
142
+
143
+ wrap.appendChild(nav);
144
+ wrap.appendChild(panels);
145
+
146
+ start.parentNode.insertBefore(wrap, start);
147
+
148
+ var cur = start;
149
+ while (cur) {
150
+ var nx = cur.nextSibling;
151
+ if (cur.parentNode) cur.parentNode.removeChild(cur);
152
+ if (cur === end) break;
153
+ cur = nx;
154
+ }
155
+ });
156
+ }
157
+
158
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', processTabs);
159
+ else processTabs();
160
+ }
161
+
162
+ /* chart
163
+ # ------------------------------------------------------------------------------------------------
164
+ */
165
+
166
+ const chart_elements = document.querySelectorAll('[id^="chart-"]');
167
+
168
+ for (const ctx of chart_elements) {
169
+ const data = ctx.dataset;
170
+
171
+ new Chart(ctx, {
172
+ type: data.type,
173
+ data: {
174
+ labels: data.labels.split(","),
175
+ datasets: [
176
+ {
177
+ label: data.label,
178
+ data: data.data.split(",").map(Number),
179
+ borderColor: data.color,
180
+ backgroundColor: `${data.color}33`,
181
+ fill: true,
182
+ tension: 0.3,
183
+ borderWidth: 2,
184
+ pointRadius: 4,
185
+ pointHoverRadius: 6
186
+ }
187
+ ]
188
+ },
189
+ options: {
190
+ responsive: true,
191
+ plugins: {
192
+ legend: {
193
+ display: true,
194
+ labels: {
195
+ color: "#444444"
196
+ }
197
+ }
198
+ },
199
+ scales: {
200
+ x: {
201
+ ticks: {
202
+ color: "#131313"
203
+ },
204
+ grid: {
205
+ color: "#111111"
206
+ }
207
+ },
208
+ y: {
209
+ ticks: {
210
+ color: "#131313"
211
+ },
212
+ grid: {
213
+ color: "#111111"
214
+ }
215
+ }
216
+ }
217
+ }
218
+ });
219
+ }
220
+
221
+ /* TOC
222
+ # ------------------------------------------------------------------------------------------------
223
+ */
224
+
225
+ const toc = document.getElementById('toc');
226
+ if (toc) {
227
+ // Variável global de largura minima do TOC
228
+ const minLayoutWidth = 1830;
229
+
230
+ const sentinel = document.createElement('div');
231
+ toc.parentNode.insertBefore(sentinel, toc);
232
+
233
+ const shouldApplyFixed = () => window.innerWidth > minLayoutWidth;
234
+
235
+ const observer = new IntersectionObserver(([entry]) => {
236
+ if (shouldApplyFixed()) {
237
+ if (!entry.isIntersecting) {
238
+ toc.classList.add('toc-fixed');
239
+ } else {
240
+ toc.classList.remove('toc-fixed');
241
+ }
242
+ } else {
243
+ toc.classList.remove('toc-fixed');
244
+ }
245
+ }, { threshold: 0 });
246
+
247
+ observer.observe(sentinel);
248
+
249
+ window.addEventListener('resize', () => {
250
+ if (!shouldApplyFixed()) toc.classList.remove('toc-fixed');
251
+ });
252
+
253
+ const slugify = (text) => {
254
+ if (!text) return '';
255
+ return text.toString().toLowerCase().trim()
256
+ .normalize('NFKD').replace(/[\u0300-\u036f]/g, '')
257
+ .replace(/[^\w\s-]/g, '')
258
+ .replace(/\s+/g, '-')
259
+ .replace(/--+/g, '-');
260
+ };
261
+
262
+ const buildTOC = (tocEl) => {
263
+ const selector = tocEl.dataset.tocSelector || '.post-content' || '.page-content';
264
+ const maxLevel = parseInt(tocEl.dataset.tocMaxLevel || '3', 10);
265
+ const offset = parseInt(tocEl.dataset.tocScrollOffset || '20', 10);
266
+
267
+ const root = document.querySelector(selector);
268
+
269
+ if (!root) {
270
+ tocEl.querySelector('.toc-empty').textContent = `Content not found (${selector})`;
271
+ tocEl.querySelector('.toc-empty').style.display = 'block';
272
+ return;
273
+ }
274
+
275
+ const headings = Array.from(
276
+ root.querySelectorAll(Array(maxLevel).fill(0).map((_, i) => `h${i + 1}`).join(','))
277
+ )
278
+ .filter((h) => !tocEl.contains(h))
279
+ .filter((h) => parseInt(h.tagName.substring(1)) <= maxLevel);
280
+
281
+ if (headings.length === 0) return;
282
+
283
+ const tocRoot = tocEl.querySelector('.toc-list');
284
+ tocRoot.innerHTML = '';
285
+
286
+ const idCounts = {};
287
+ for (const h of headings) {
288
+ if (!h.id) {
289
+ let id = slugify(h.textContent);
290
+ if (!id) id = 'section';
291
+
292
+ if (idCounts[id]) {
293
+ idCounts[id] += 1;
294
+ id = `${id}-${idCounts[id]}`;
295
+ } else {
296
+ idCounts[id] = 1;
297
+ }
298
+
299
+ h.id = id;
300
+ }
301
+ }
302
+
303
+ const stack = [{ level: 0, ul: tocRoot }];
304
+ for (let i = 0; i < headings.length; i++) {
305
+ const h = headings[i];
306
+ const level = parseInt(h.tagName.substring(1));
307
+ const li = document.createElement('li');
308
+ const a = document.createElement('a');
309
+ a.href = `#${h.id}`;
310
+ a.textContent = h.textContent.trim();
311
+
312
+ a.addEventListener('click', (e) => {
313
+ e.preventDefault();
314
+ window.scrollTo({
315
+ top: h.getBoundingClientRect().top + window.scrollY - offset,
316
+ behavior: 'smooth'
317
+ });
318
+ history.replaceState(null, '', `#${h.id}`);
319
+ });
320
+
321
+ li.appendChild(a);
322
+
323
+ while (stack.length > 1 && level <= stack[stack.length - 1].level) {
324
+ stack.pop();
325
+ }
326
+
327
+ const parent = stack[stack.length - 1].ul;
328
+ parent.appendChild(li);
329
+
330
+ const next = headings[i + 1];
331
+ if (next) {
332
+ const nextLevel = parseInt(next.tagName.substring(1));
333
+ if (nextLevel > level) {
334
+ const newUl = document.createElement('ul');
335
+ li.appendChild(newUl);
336
+ stack.push({ level, ul: newUl });
337
+ }
338
+ }
339
+ }
340
+
341
+ const links = tocRoot.querySelectorAll('a');
342
+ const onScroll = () => {
343
+ const fromTop = window.scrollY + offset + 1;
344
+ let current = headings[0];
345
+
346
+ for (const h of headings) {
347
+ if (h.offsetTop <= fromTop) current = h;
348
+ }
349
+
350
+ for (const l of links) {
351
+ l.classList.toggle('active', l.getAttribute('href') === `#${current.id}`);
352
+ }
353
+ };
354
+
355
+ window.addEventListener('scroll', onScroll, { passive: true });
356
+ onScroll();
357
+ };
358
+
359
+ for (const tocEl of document.querySelectorAll('.toc')) {
360
+ // Obtém os textos dos botões do dataset (agora dinâmicos)
361
+ const btnShowText = tocEl.dataset.btnShow || 'Show';
362
+ const btnHiddenText = tocEl.dataset.btnHidden || 'Hide';
363
+
364
+ buildTOC(tocEl);
365
+
366
+ const toggle = tocEl.querySelector('.toc-toggle');
367
+ const wrapper = tocEl.querySelector('.toc-list-wrapper');
368
+
369
+ wrapper.style.display = 'none';
370
+ toggle.setAttribute('aria-expanded', 'false');
371
+ toggle.textContent = btnShowText;
372
+
373
+ toggle.addEventListener('click', () => {
374
+ const expanded = toggle.getAttribute('aria-expanded') === 'true';
375
+ wrapper.style.display = expanded ? 'none' : 'block';
376
+ toggle.setAttribute('aria-expanded', (!expanded).toString());
377
+ // Define o texto dinamicamente
378
+ toggle.textContent = expanded ? btnShowText : btnHiddenText;
379
+ });
380
+
381
+ const tocTop = tocEl.offsetTop;
382
+
383
+ const handleScrollFix = () => {
384
+ if (window.innerWidth <= minLayoutWidth) {
385
+ tocEl.classList.remove('fixed');
386
+ tocEl.style.position = '';
387
+ tocEl.style.top = '';
388
+ tocEl.style.zIndex = '';
389
+ tocEl.style.width = '';
390
+ return;
391
+ }
392
+
393
+ const scrollTop = window.scrollY || document.documentElement.scrollTop;
394
+
395
+ if (scrollTop >= tocTop) {
396
+ tocEl.classList.add('fixed');
397
+ tocEl.style.position = 'fixed';
398
+ tocEl.style.top = '0';
399
+ tocEl.style.zIndex = '9999';
400
+ } else {
401
+ tocEl.classList.remove('fixed');
402
+ tocEl.style.position = '';
403
+ tocEl.style.top = '';
404
+ tocEl.style.width = '';
405
+ }
406
+ };
407
+
408
+ // fechar TOC ao pressionar 'Esc'
409
+ document.addEventListener('keydown', (e) => {
410
+ if (e.key === 'Escape') {
411
+ wrapper.style.display = 'none';
412
+ toggle.setAttribute('aria-expanded', 'false');
413
+ toggle.textContent = btnShowText;
414
+ }
415
+ });
416
+
417
+ window.addEventListener('scroll', handleScrollFix, { passive: true });
418
+ window.addEventListener('resize', handleScrollFix);
419
+ handleScrollFix();
420
+ }
421
+ }
422
+
423
+ });
@@ -0,0 +1 @@
1
+ document.addEventListener("DOMContentLoaded", () => {});