再也不随便起名了

这下真的阵地做成E-了

再也不随便起名了

前言

阵地作成】Fate系列作品中Caster及其余职介个别从者的保有技能,专指用于制作阵地以收集魔力的能力。本博客在月批博主的操控下援引这一定义,并在周年更新时新建“阵地做成E-”tag作为专门收纳装修记录的一个标签。

阵地做成:装修也是一种制作魔术阵地。

E-:我菜。

当时写出这个的时候还蛮沾沾自喜的,心想嘿嘿这个tag名可好玩了。结果我马上就在随后而来的一次博客大装修中狠狠地吃瘪了。

这下真阵地做成E-了,一语成谶……妈妈啊我错了我再也不随便起名字了(哭

记录

我写游戏记录主打一个再不写就忘了,我写阅读观影记录主打一个再不写就忘了,我写装修记录仍旧在以再不写就忘了为主题,难道我写博客的终极目的其实是防止老年痴呆

实际上在写这篇博客的时候才意识到的,我应该给装修记录来个装修前VS装修后的对比。但是没留。

也懒得管了(?)

以下memo部分展现出了很美的代码0基础人尝试驯服网页的珍贵录像,GO。

零散装修其一

chatgpt听我说谢谢你

其实最近装修热情这么高的一部分原因是chatgpt的介入。之前看友友们装修的时候都在用它帮忙,当时的想法是,”嗯反正我也不会去装修的吧…”所以就没去倒腾。直到上半年图床报废+多图博客稳定报错,我才转向这位赛博哆啦A梦求助。求助完之后发现……

太好用了……

到现在我已经发展为: 把所有的需求哐哐地丢过去然后让ai自己想办法。没想到这样一来,我算是在一个新的层面上理解了为什么ai会对人类的就业产生如此之大的影响:

他们真的是完美的乙方……他们,真的是完美的乙方……

改动:短代码试验场

友链页面的描述部分之前一直觉得还是不够有趣,所以想利用对话框短代码将“来交换友链吧!“从文本的形式,转为对话的形式

热情程度嘭嘭上涨了不是吗~

基本上是搬运代码,memo一下遇到的问题。

一开始是直接复制粘贴,但是出现了很神秘的问题……它自己给我生成了一个奇怪的头像…?(甚至不是教程中模板里那个自带的)。于是乎踏上漫漫解决(骚扰chatgpt)路:

全部一问才知道源代码并不能完美cover我对于”对话框显示效果“的需求。那么接下来就轮到ai替我魔改了。最终出来的,能够自定义头像的源码(by chatgpt)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<link rel="stylesheet" href="/css/message.css" />

<div class="message {{ if eq (.Get "from") "self" }}--self{{ end }}">
    <div class="message__inner">
        {{ with .Get "avatar" }}
        <!-- 使用自定义头像 -->
        <img class="avatar" src="{{ . }}" width="24" height="24">
        {{ else if (ne (.Get "accountID") "") }}
        <!-- 使用 accountID 生成头像 -->
        <img class="avatar" src="https://unavatar.io/{{ .Get "accountID" }}" width="24" height="24">
        {{ else }}
        <!-- 默认隐藏头像 -->
        <img class="avatar" src="" width="24" height="24" style="visibility:hidden;">
        {{ end }}
        <div class="message__text">
            <hstack class="message__meta">{{ .Get "name" }} <spacer></spacer> {{ .Get "timestamp" }}</hstack>
            {{ .Inner }}

            {{ if ne (.Get "images") "" }}
            <div class="grid-container">
                {{ range split (.Get "images") "," }}
                <div class="grid-item">
                    <img class="message__img" src="{{ . }}" alt="Image">
                </div>
                {{ end }}
            </div>
            {{ end }}
        </div>
    </div>
</div>

我喜出望外并修改deploy,报错。

具体的报错信息:Error: add site dependencies: load resources: loading templates: “/vercel/path0/themes/hugo-theme-stack/layouts/shortcodes/message.html:8:1”: parse failed: template: shortcodes/message.html:8: unexpected in input

第八行"{{ else if (ne (.Get “accountID”) “") }}“出错。把错误丢给chatgpt改,发回了一个一模一样的……

就这么来回往复了几次吧,这第八行总会报错,但是chatgpt发回的代码总是有这第八行。于是乎我决定破釜沉舟之,直接删去这第八行,再不行我就不搞这对话框了。

然后成功了😳

这算不算是一种,解决不了问题就解决(杀掉)产生问题的人……

魔改后的原文引入方式:

{< message from=“self” accountID=“twitter/昵称” images=“图片链接” timestamp=“自定义时间” name=“name” avatar=“自定义头像链接” >} 这里是自定义的信息内容。 {< /message >}

*使用时请双括号!

  • 自定义scss(某种意义上成功了,但是某种意义上又没成功。还好结果也不是不能接受。scss这玩意真心看得我头大)

源css在正文里使用是没有问题的,但是如果作为友链callout的话,头像就小的可怜……

在头像定义字段进行修改!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
img.avatar{
  position: relative;
  width: 96px;
  height: 96px;
  overflow: hidden;
  border-radius: 48px;
  box-shadow: inset 0 0 0 1px rgba(105, 105, 105, 0.1);
  flex-shrink: 0;
  margin:unset !important;
}

然后就是现在的大小了,顺便也学习了像素到厘米和英寸的单位转换:

计算方法

  1. 像素到英寸的转换
    • 1 英寸 = 2.54 厘米
    • 如果屏幕的 PPI 是 96,则 1 像素 = 1 / 96 英寸
  2. 像素到厘米的转换
    • 像素到英寸的转换:96 像素 = 96 / 96 = 1 英寸
    • 1 英寸 = 2.54 厘米

总之img.avatar这段的三个数字,radious这儿必须是前两个的一半,不然就不是圆了hhhhh

总之请看。头像选用了我的漂亮母肥!

QQ20240914223004.png

母肥虽然很可口可爱,但是这个怎么看都不太像一个……”对话框“吧……

感觉可能是自定义css时出了问题,或者说魔改过的对话框代码不适配原对话框代码的css。但是这个效果我也不是不能接受,都能显示这么可爱的母肥了!

知足常乐激流勇退,就先这样吧(吧 )。

引入Pjax实现歌曲播放不中断

开端是看到了了这个教程,其实引入歌单的时候就看到了,部署完歌单欣赏了几天效果,发现切换页面的时候整个歌单就停掉确实不太舒服,成功触发了我的强迫症。含泪开启pjax大工程。

记录一下流水账过程

按照教程一步一步走,歌曲不间断(✅),引入pjax(✅),文章样式修复(✅),主题颜色切换(✅)教程里的评论区不是waline?去吧chatgpt!评论区修复(❓)评论区修复(❓)评论区修复(❓)

……

评论区修复(❓❓❓)

汗流浃背了兄弟。waline你怎么回事?不论我怎么改需求chatgpt怎么写,我的评论就是要二次刷新才能缓缓出现。坐大牢!颠三倒四地改了很久,增加的只有很多很多的无用功。

精神状态:

KOXLNWFAE42ONG_MZF_58M.jpg

实在没办法了,使用光之摇人唤来了已经改完pjax出狱的爱海老师进来抬我过本。看了眼爱海老师的装修博客,fuji主题里的js,有一段代码是用于根据当前页面的评论区的状态和类型加载评论内容。stack里有吗?stack里…有吗?!

是有的

QQ20240914223439.png

在这里。

改好第一轮之后评论区终于是存活下来,同时搜索也一并被海哥复活了,赞美爱海!但是这时候又出现了第二个问题,评论区平等地出现在了每一个页面下方(这效果其实还挺好笑的www不过没有截图了,黑历史影像不予留存!)

于是乎再改,这一轮改动了waline.html和single.html,改完之后成功了!不过【这么改的话,以后定义waline相关设置就不能通过config了,而是waline.html和定义pjax的html文件两边都需要修改】

改完之后成功了吗?还没有……部署上去之后出现了又一个问题,我的虚拟进度条无限加载ing…最后改动了以下topbar.min.js保存的位置然后修改了pjax定义文件,我的博客终于——

活!!!过!!!来!!!!了!!!!!

是幻觉吗,不是幻觉,真的修好了,大感动。

大感动!!!!!

上结果

  • 评论区修复用

comments-waline.html替换为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<!-- 在页面中放置 Waline 的 CSS 和容器 -->
<link href='//unpkg.com/@waline/client@v2/dist/waline.css' rel='stylesheet'/>
<div id="waline" class="waline-container"></div>
<style>
    .waline-container {
        background-color: var(--card-background);
        border-radius: var(--card-border-radius);
        box-shadow: var(--shadow-l1);
        padding: var(--card-padding);
        --waline-font-size: var(--article-font-size);
    }
    .waline-container .wl-count {
        color: var(--card-text-color-main);
    }
</style>

<script type="module">
    // 封装 Waline 初始化函数
    async function initializeWaline() {
        // 动态引入 Waline 的 JavaScript 文件
        const { init } = await import('https://unpkg.com/@waline/client@v2/dist/waline.mjs');
        
        init({
            el: '#waline',
            serverURL: 'https://cirispacee.vercel.app/',
            dark: 'html[data-theme="dark"]',
            requiredMeta: ['nick', 'mail'],
            search: false,
            locale: {
                admin: 'admin',
                placeholder: '请在这里输入希望的文字。'
            },
            emoji: [
                  'https://gcore.jsdelivr.net/gh/Saidosi/azuki-emoji-for-waline@1.0/azukisan/',
                  'https://gcore.jsdelivr.net/gh/norevi/waline-blobcatemojis@1.0/blobs',
             ]
        });
    }

    // 在页面加载时初始化 Waline
    initializeWaline();

    // 监听 PJAX 完成事件,并在 PJAX 请求完成后重新初始化 Waline
    document.addEventListener('pjax:complete', () => {
        initializeWaline();
    });
</script>

layout\default\single.html替换为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
{{ define "body-class" }}
    article-page
    {{/* 
        Enable the right sidebar if
            - Widget different from 'TOC' is enabled
            - TOC is enabled and not empty
    */}}
    {{- $HasWidgetNotTOC := false -}}
    {{- $TOCWidgetEnabled := false -}}
    {{- range .Site.Params.widgets.page -}}
        {{- if ne .type "toc" -}}
            {{ $HasWidgetNotTOC = true -}}
        {{- else -}}
            {{ $TOCWidgetEnabled = true -}}
        {{- end -}}
    {{- end -}}

    {{- $TOCManuallyDisabled := eq .Params.toc false -}}
    {{- $TOCEnabled := and (not $TOCManuallyDisabled) $TOCWidgetEnabled -}}
    {{- $hasTOC := ge (len .TableOfContents) 100 -}}
    {{- .Scratch.Set "TOCEnabled" (and $TOCEnabled $hasTOC) -}}
    
    {{- .Scratch.Set "hasWidget" (or $HasWidgetNotTOC (and $TOCEnabled $hasTOC)) -}}
{{ end }}

{{ define "main" }}
    {{ partial "article/article.html" . }}

    {{ if .Params.links }}
        {{ partial "article/components/links" . }}
    {{ end }}

    {{ partial "article/components/related-content" . }}
     
    {{ if not (eq .Params.comments false) }}
        {{ partial "comments/provider/waline" . }}
    {{ end }}

    {{ partialCached "footer/footer" . }}

    {{ partialCached "article/components/photoswipe" . }}
{{ end }}

{{ define "right-sidebar" }}
    {{ if .Scratch.Get "hasWidget" }}{{ partial "sidebar/right.html" (dict "Context" . "Scope" "page") }}{{ end}}
{{ end }}

  • 引入虚拟进度条

下载:(download zip),把解压后的topbar.min.js放到/static/js里(没有就新建)

pjax引入及相关修复

在partial下创建custom.html用于引入pjax,并在baseof内对custom.html进行引入(但是不建议这么起名,感觉custom.html到处都是,我改的后面大脑混沌了都怕自己弄错。)

  • 包含音乐连续播放+pjax引入+文章样式修复+主题切换修复+虚拟进度条引入+waline评论区修复最终版的custom.html:
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
<script src="/js/topbar.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/pjax/pjax.min.js"></script>

<script>
	var pjax = new Pjax({
	  selectors: [
	    ".main-container",
	  ]
	})
</script>

<script>
    function initializeWaline() {
            if (document.querySelector('#waline')) {
                import('https://unpkg.com/@waline/client@v2/dist/waline.mjs').then(({ init }) => {
                    init({
            el: '#waline',
            serverURL: 'https://cirispacee.vercel.app/',
            dark: 'html[data-scheme="dark"]',
            requiredMeta: ['nick', 'mail'],
            search: false,
            locale: {
                admin: 'admin',
                placeholder: '请在这里输入希望的文字。'
            },
            emoji: [
                  'https://gcore.jsdelivr.net/gh/Saidosi/azuki-emoji-for-waline@1.0/azukisan/',
                  'https://gcore.jsdelivr.net/gh/norevi/waline-blobcatemojis@1.0/blobs',
             ]
        });
                });
            }
        }
    
    
    
        // 在页面初次加载时初始化 Waline
        document.addEventListener('DOMContentLoaded', initializeWaline);
    
        // 监听 PJAX 完成事件并初始化 Waline
        document.addEventListener('pjax:complete', initializeWaline);
</script>

<!-- 【custom.html】 -->
<script>
    /**
     * 页面销毁前监听
     */
    window.onbeforeunload = () => {
        // 将播放信息用对象封装,并存入到localStorage中
        const playInfo = {
            index: ap.list.index,
            currentTime: ap.audio.currentTime,
            paused: ap.paused
        };
        localStorage.setItem("playInfo", JSON.stringify(playInfo));
    };

    /**
     * 页面加载后监听
     */
    window.onload = () => {
        // 从localStorage取出播放信息
        const playInfo = JSON.parse(localStorage.getItem("playInfo"));
        if (!playInfo) {
            return;
        }
        // 切换歌曲
        ap.list.switch(playInfo.index);
        // 等待500ms再执行下一步(切换歌曲需要点时间,不能立马调歌曲进度条)
        setTimeout(() => {
            // 调整时长
            ap.seek(playInfo.currentTime);
            // 是否播放
            if (!playInfo.paused) {
                ap.play()
            }
        }, 500);
    };
</script>

<!-- 【custom.html】 文章样式修复-->
<script>
    document.addEventListener('DOMContentLoaded', () => {
        // 手动设置初始页面的 body class
        const bodyClass = document.querySelector('body')?.className;
        if (bodyClass) {
            document.body.setAttribute('class', bodyClass);
        }
    });

    // Pjax 处理
    pjax._handleResponse = pjax.handleResponse;
    pjax.handleResponse = function(responseText, request, href, options) {
        if (request.responseText.match("<html")) {
            let newDom = new DOMParser().parseFromString(responseText, 'text/html');
            let bodyClass = newDom.body.className;
            document.body.setAttribute("class", bodyClass);
            pjax._handleResponse(responseText, request, href, options);
        } else {
            pjax._handleResponse(responseText, request, href, options);
        }
    }
</script>
<!-- 【custom.html】 主题颜色切换-->

<script>
    document.addEventListener('pjax:complete', () => {
        // Stack脚本初始化
        window.Stack.init();
    })
</script>

<script>
  // 修改进度条颜色
  topbar.config({
      barColors: {
          '0': 'rgba(138, 162, 211, 1)', 
          '1.0': 'rgba(138, 162, 211,  1)' 
      }
  })

  document.addEventListener('pjax:send', () => {
      // 显示顶部进度条
      topbar.show();
  })

  document.addEventListener('pjax:complete', () => {
      // 隐藏顶部进度条
      topbar.hide();
  })
</script>

  • default-baseof引入
1
 {{ partial "custom.html" . }}
  • 用于搜索修改的main.ts与search.tsx

main.ts:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
/*!
*   Hugo Theme Stack
*
*   @author: Jimmy Cai
*   @website: https://jimmycai.com
*   @link: https://github.com/CaiJimmy/hugo-theme-stack
*/
import StackGallery from "ts/gallery";
import { getColor } from 'ts/color';
import menu from 'ts/menu';
import createElement from 'ts/createElement';
import StackColorScheme from 'ts/colorScheme';
import { setupScrollspy } from 'ts/scrollspy';
import { setupSmoothAnchors } from "ts/smoothAnchors";
import { searchInit } from "ts/search";

let Stack = {
    init: () => {
        /**
         * Bind menu event
         */
        menu();
        searchInit();

        const articleContent = document.querySelector('.article-content') as HTMLElement;
        if (articleContent) {
            new StackGallery(articleContent);
            setupSmoothAnchors();
            setupScrollspy();
        }

        /**
         * Add linear gradient background to tile style article
         */
        const articleTile = document.querySelector('.article-list--tile');
        if (articleTile) {
            let observer = new IntersectionObserver(async (entries, observer) => {
                entries.forEach(entry => {
                    if (!entry.isIntersecting) return;
                    observer.unobserve(entry.target);

                    const articles = entry.target.querySelectorAll('article.has-image');
                    articles.forEach(async articles => {
                        const image = articles.querySelector('img'),
                            imageURL = image.src,
                            key = image.getAttribute('data-key'),
                            hash = image.getAttribute('data-hash'),
                            articleDetails: HTMLDivElement = articles.querySelector('.article-details');

                        const colors = await getColor(key, hash, imageURL);

                        articleDetails.style.background = `
                        linear-gradient(0deg, 
                            rgba(${colors.DarkMuted.rgb[0]}, ${colors.DarkMuted.rgb[1]}, ${colors.DarkMuted.rgb[2]}, 0.5) 0%, 
                            rgba(${colors.Vibrant.rgb[0]}, ${colors.Vibrant.rgb[1]}, ${colors.Vibrant.rgb[2]}, 0.75) 100%)`;
                    })
                })
            });

            observer.observe(articleTile)
        }

        /**
         * Add copy button to code block
        */
        const highlights = document.querySelectorAll('.article-content div.highlight');
        const copyText = `Copy`,
            copiedText = `Copied!`;

        highlights.forEach(highlight => {
            const copyButton = document.createElement('button');
            copyButton.innerHTML = copyText;
            copyButton.classList.add('copyCodeButton');
            highlight.appendChild(copyButton);

            const codeBlock = highlight.querySelector('code[data-lang]');
            if (!codeBlock) return;

            copyButton.addEventListener('click', () => {
                navigator.clipboard.writeText(codeBlock.textContent)
                    .then(() => {
                        copyButton.textContent = copiedText;

                        setTimeout(() => {
                            copyButton.textContent = copyText;
                        }, 1000);
                    })
                    .catch(err => {
                        alert(err)
                        console.log('Something went wrong', err);
                    });
            });
        });

        new StackColorScheme(document.getElementById('dark-mode-toggle'));
    }
}

window.addEventListener('load', () => {
    setTimeout(function () {
        Stack.init();
    }, 0);
})

declare global {
    interface Window {
        createElement: any;
        Stack: any
    }
}

window.Stack = Stack;
window.createElement = createElement;

search.tsx:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
interface pageData {
    title: string,
    date: string,
    permalink: string,
    content: string,
    image?: string,
    preview: string,
    matchCount: number
}

interface match {
    start: number,
    end: number
}

/**
 * Escape HTML tags as HTML entities
 * Edited from:
 * @link https://stackoverflow.com/a/5499821
 */
const tagsToReplace = {
    '&': '&amp;',
    '<': '&lt;',
    '>': '&gt;',
    '"': '&quot;',
    '…': '&hellip;'
};

function replaceTag(tag) {
    return tagsToReplace[tag] || tag;
}

function replaceHTMLEnt(str) {
    return str.replace(/[&<>"]/g, replaceTag);
}

function escapeRegExp(string) {
    return string.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&');
}

class Search {
    private data: pageData[];
    private form: HTMLFormElement;
    private input: HTMLInputElement;
    private list: HTMLDivElement;
    private resultTitle: HTMLHeadElement;
    private resultTitleTemplate: string;

    constructor({ form, input, list, resultTitle, resultTitleTemplate }) {
        this.form = form;
        this.input = input;
        this.list = list;
        this.resultTitle = resultTitle;
        this.resultTitleTemplate = resultTitleTemplate;

        this.handleQueryString();
        this.bindQueryStringChange();
        this.bindSearchForm();
    }

    /**
     * Processes search matches
     * @param str original text
     * @param matches array of matches
     * @param ellipsis whether to add ellipsis to the end of each match
     * @param charLimit max length of preview string
     * @param offset how many characters before and after the match to include in preview
     * @returns preview string
     */
    private static processMatches(str: string, matches: match[], ellipsis: boolean = true, charLimit = 140, offset = 20): string {
        matches.sort((a, b) => {
            return a.start - b.start;
        });

        let i = 0,
            lastIndex = 0,
            charCount = 0;

        const resultArray: string[] = [];

        while (i < matches.length) {
            const item = matches[i];

            /// item.start >= lastIndex (equal only for the first iteration)
            /// because of the while loop that comes after, iterating over variable j

            if (ellipsis && item.start - offset > lastIndex) {
                resultArray.push(`${replaceHTMLEnt(str.substring(lastIndex, lastIndex + offset))} [...] `);
                resultArray.push(`${replaceHTMLEnt(str.substring(item.start - offset, item.start))}`);
                charCount += offset * 2;
            }
            else {
                /// If the match is too close to the end of last match, don't add ellipsis
                resultArray.push(replaceHTMLEnt(str.substring(lastIndex, item.start)));
                charCount += item.start - lastIndex;
            }

            let j = i + 1,
                end = item.end;

            /// Include as many matches as possible
            /// [item.start, end] is the range of the match
            while (j < matches.length && matches[j].start <= end) {
                end = Math.max(matches[j].end, end);
                ++j;
            }

            resultArray.push(`<mark>${replaceHTMLEnt(str.substring(item.start, end))}</mark>`);
            charCount += end - item.start;

            i = j;
            lastIndex = end;

            if (ellipsis && charCount > charLimit) break;
        }

        /// Add the rest of the string
        if (lastIndex < str.length) {
            let end = str.length;
            if (ellipsis) end = Math.min(end, lastIndex + offset);

            resultArray.push(`${replaceHTMLEnt(str.substring(lastIndex, end))}`);

            if (ellipsis && end != str.length) {
                resultArray.push(` [...]`);
            }
        }

        return resultArray.join('');
    }

    private async searchKeywords(keywords: string[]) {
        const rawData = await this.getData();
        const results: pageData[] = [];

        const regex = new RegExp(keywords.filter((v, index, arr) => {
            arr[index] = escapeRegExp(v);
            return v.trim() !== '';
        }).join('|'), 'gi');

        for (const item of rawData) {
            const titleMatches: match[] = [],
                contentMatches: match[] = [];

            let result = {
                ...item,
                preview: '',
                matchCount: 0
            }

            const contentMatchAll = item.content.matchAll(regex);
            for (const match of Array.from(contentMatchAll)) {
                contentMatches.push({
                    start: match.index,
                    end: match.index + match[0].length
                });
            }

            const titleMatchAll = item.title.matchAll(regex);
            for (const match of Array.from(titleMatchAll)) {
                titleMatches.push({
                    start: match.index,
                    end: match.index + match[0].length
                });
            }

            if (titleMatches.length > 0) result.title = Search.processMatches(result.title, titleMatches, false);
            if (contentMatches.length > 0) {
                result.preview = Search.processMatches(result.content, contentMatches);
            }
            else {
                /// If there are no matches in the content, use the first 140 characters as preview
                result.preview = replaceHTMLEnt(result.content.substring(0, 140));
            }

            result.matchCount = titleMatches.length + contentMatches.length;
            if (result.matchCount > 0) results.push(result);
        }

        /// Result with more matches appears first
        return results.sort((a, b) => {
            return b.matchCount - a.matchCount;
        });
    }

    private async doSearch(keywords: string[]) {
        const startTime = performance.now();

        const results = await this.searchKeywords(keywords);
        this.clear();

        for (const item of results) {
            this.list.append(Search.render(item));
        }

        const endTime = performance.now();

        this.resultTitle.innerText = this.generateResultTitle(results.length, ((endTime - startTime) / 1000).toPrecision(1));

        pjax.refresh(document);
    }

    private generateResultTitle(resultLen, time) {
        return this.resultTitleTemplate.replace("#PAGES_COUNT", resultLen).replace("#TIME_SECONDS", time);
    }

    public async getData() {
        if (!this.data) {
            /// Not fetched yet
            const jsonURL = this.form.dataset.json;
            this.data = await fetch(jsonURL).then(res => res.json());
            const parser = new DOMParser();

            for (const item of this.data) {
                item.content = parser.parseFromString(item.content, 'text/html').body.innerText;
            }
        }

        return this.data;
    }

    private bindSearchForm() {
        let lastSearch = '';

        const eventHandler = (e) => {
            e.preventDefault();
            const keywords = this.input.value.trim();

            Search.updateQueryString(keywords, true);

            if (keywords === '') {
                lastSearch = '';
                return this.clear();
            }

            if (lastSearch === keywords) return;
            lastSearch = keywords;

            this.doSearch(keywords.split(' '));
        }

        this.input.addEventListener('input', eventHandler);
        this.input.addEventListener('compositionend', eventHandler);
    }

    private clear() {
        this.list.innerHTML = '';
        this.resultTitle.innerText = '';
    }

    private bindQueryStringChange() {
        window.addEventListener('popstate', (e) => {
            this.handleQueryString()
        })
    }

    private handleQueryString() {
        const pageURL = new URL(window.location.toString());
        const keywords = pageURL.searchParams.get('keyword');
        this.input.value = keywords;

        if (keywords) {
            this.doSearch(keywords.split(' '));
        }
        else {
            this.clear()
        }
    }

    private static updateQueryString(keywords: string, replaceState = false) {
        const pageURL = new URL(window.location.toString());

        if (keywords === '') {
            pageURL.searchParams.delete('keyword')
        }
        else {
            pageURL.searchParams.set('keyword', keywords);
        }

        if (replaceState) {
            window.history.replaceState('', '', pageURL.toString());
        }
        else {
            window.history.pushState('', '', pageURL.toString());
        }
    }

    public static render(item: pageData) {
        return <article>
            <a href={item.permalink}>
                <div class="article-details">
                    <h2 class="article-title" dangerouslySetInnerHTML={{ __html: item.title }}></h2>
                    <section class="article-preview" dangerouslySetInnerHTML={{ __html: item.preview }}></section>
                </div>
                {item.image &&
                    <div class="article-image">
                        <img src={item.image} loading="lazy" />
                    </div>
                }
            </a>
        </article>;
    }
}

declare global {
    interface Window {
        searchResultTitleTemplate: string;
    }
}

//window.addEventListener('load', () => {

function searchInit() {
    let search = document.querySelector('.search-result');
    if (search) {
        const searchForm = document.querySelector('.search-form') as HTMLFormElement,
            searchInput = searchForm.querySelector('input') as HTMLInputElement,
            searchResultList = document.querySelector('.search-result--list') as HTMLDivElement,
            searchResultTitle = document.querySelector('.search-result--title') as HTMLHeadingElement;

        new Search({
            form: searchForm,
            input: searchInput,
            list: searchResultList,
            resultTitle: searchResultTitle,
            resultTitleTemplate: window.searchResultTitleTemplate
        });
    }
}

export {
    searchInit
}

export default Search;

script.html,覆盖layouts\partials\footer\components中的script.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
{{- partial "helper/external" (dict "Context" . "Namespace" "Vibrant") -}}

{{- $opts := dict "minify" hugo.IsProduction "JSXFactory" "createElement" -}}
{{- $script := resources.Get "ts/main.ts" | js.Build $opts -}}

<script type="text/javascript" src="{{ $script.RelPermalink }}" defer></script>

{{- with resources.Get "ts/custom.ts" -}}
    {{/* Place your custom script in HUGO_SITE_FOLDER/assets/ts/custom.ts */}}
    {{- $customScript := . | js.Build $opts -}}
    <script type="text/javascript" src="{{ $customScript.RelPermalink }}" defer></script>
{{- end -}}

不知道哪里有用所以都丢上来了.jpg。if有人需要参考的话…问我为啥我是说不出来的。但是按着这个来能行。能行就好!

Special thanks to

海哥(不是)爱海老师,我的小博客最后还是活出了人形80%的功劳都要归功于伟大的哆啦爱A海(剩下20%给chatgpt(x))。太厉害了海哥,上午看到教程,下午成为教程,晚上超越教程。

留下一点小白鼠测试博客的影像:

QQ20240914205429.jpg

因为我把themes一起打包发给海哥了,所以小白鼠博客也一并拥有了我的头像与歌单(甚至也能实时更新,笑死,什么英雄的掠影)

装修完了小白鼠博客也暂时下班了。

【英雄的掠影离开了……】

TO 海哥:

IMG_5907.jpeg

博客大师!

零散装修其二

子博客之前一直加载不出favicon,错怪notion了,原来是我图片放错地方了(放到了主目录下)移到了public下,有了! 请用立耳博客与趴趴耳子博客 IMG_5785.jpeg

为什么不放进其一里,因为我差点把这码事给忘了。

记录希望的影像
Built with Hugo
主题 StackJimmy 设计