Full Code of Coffcer/coffce-pjax for AI

master 6b78f347a14f cached
10 files
20.4 KB
6.0k tokens
1 requests
Download .txt
Repository: Coffcer/coffce-pjax
Branch: master
Commit: 6b78f347a14f
Files: 10
Total size: 20.4 KB

Directory structure:
gitextract_aaoaea37/

├── .brackets.json
├── .gitattributes
├── README.md
├── demo/
│   ├── 1.html
│   ├── 2.html
│   ├── 3.html
│   └── index.html
├── package.json
├── scripts/
│   └── coffce-pjax.js
└── styles/
    └── style.css

================================================
FILE CONTENTS
================================================

================================================
FILE: .brackets.json
================================================
{
    "sbruchmann.staticpreview.basepath": "G:/轮子/coffce-pjax/"
}

================================================
FILE: .gitattributes
================================================
# Auto detect text files and perform LF normalization
* text=auto

# Custom for Visual Studio
*.cs     diff=csharp

# Standard to msysgit
*.doc	 diff=astextplain
*.DOC	 diff=astextplain
*.docx diff=astextplain
*.DOCX diff=astextplain
*.dot  diff=astextplain
*.DOT  diff=astextplain
*.pdf  diff=astextplain
*.PDF	 diff=astextplain
*.rtf	 diff=astextplain
*.RTF	 diff=astextplain


================================================
FILE: README.md
================================================
coffce-pjax
===
coffce-pjax可以将页面所有的跳转替换为AJAX请求,把网站改造成单页面应用。<br>
note: 由于浏览器限制,pjax需要在服务器环境下使用,即不要使用file://xxx.html运行。

###有何用处:
* 可以在页面切换间平滑过渡,增加Loading动画。
* 可以在各个页面间传递数据,不依赖URL。
* 可以选择性的保留状态,如音乐网站,切换页面时不会停止播放歌曲。
* 所有的标签都可以用来跳转,不仅仅是a标签。
* 避免了公共JS的反复执行,如无需在各个页面打开时都判断是否登录过等等。
* 减少了请求体积,节省流量,加快页面响应速度。
* 平滑降级到低版本浏览器上,对SEO也不会有影响。

###兼容性:
* Chrome, Firefox, Safari, Android Browser, IE8+等。
* 在IE8和IE9上使用URL Hash,即地址栏的#号。
* 在更低版本的浏览器和搜索引擎蜘蛛上,保持默认跳转,不受影响。

如何使用
---
####安装:
    npm install coffce-pjax

#### 引入
``` javascript
// 使用全局变量
var pjax = window.CoffcePJAX
```

``` javascript
// 使用commonJS或AMD
var pjax = require("coffce-pjax");
```
####简单配置:
``` javascript
pjax.init({
    // 替换新页面内容的容器
    container: "body",
    // 是否在低版本浏览器上使用Hash
    hash: true
});
```
####完整配置:
``` javascript
pjax.init({
    // 选择器,支持querySelector选择器
    selector: "a",
    // 要替换内容的容器,可为选择器字符串或DOM对象
    container: "body",
    // 是否在前进后退时开启本地缓存功能
    cache : true,
    // 是否对低版本浏览器启用hash方案,不启用此项的低版本浏览器则会按照普通模式跳转
    hash: false,
    // 是否允许跳转到当前相同URL,相当于刷新
    same: true,
    // 调试模式,console.log调试信息
    debug: false,
    
    // 各个执行阶段的过滤函数,返回false则停止pjax执行
    filter: {
        // 选择器过滤,如果querySelector无法满足需求,可以在此函数里二次过滤
        selector: function(a) {},
        // 接收到ajax请求返回的内容时触发
        content: function(title, html) {}
    },
    // 各个阶段的自定义函数,将代替默认函数
    custom: {
        // 自定义更换页面函数,可以在此实现动画效果等
        append: function(html, container) {}
    },
    // 要监听的事件,相当于pjax.on(...),事件列表看下面
    events: {}
});
```

接口
---
```javascript
/**
 * 初始化
 * @param {Object} options 配置,详情见上面↑
 */
pjax.init(config);
```

```javascript
 // 注销插件,一般来说你不需要使用这个方法
pjax.destroy();
```

```javascript
/**
 * 使用pjax跳转到指定页面
 * @param {String}   url
 * @param {Object}   data     要传到新页面的参数,可以为null或undefined
 * @param {Function} callback 请求成功时的回调,可以为null或undefined
 */
pjax.turn(url, data, callback);
```

```javascript
/**
 * 监听事件,事件类型见下面↓
 * @param {String}   type     事件类型
 * @param {Function} listener 回调
 * @param {String}   url      只监听某个url,可以是相对和绝对路径
 */
pjax.on(type, listener);
pjax.on(type, url, listener);
```

```javascript
/**
 * 解除监听
 * @param {String} type 事件类型
 * @param {String} url  只监听某个url,可以是相对和绝对路径
 */
pjax.off(type);
pjax.off(type, url);
```

```javascript
/**
 * 触发事件
 * @param {String} type 事件类型
 * @param {Object} args 参数
 */
pjax.trigger(type, args);
```

事件
---
####监听事件
```javascript
// 通过接口监听
pjax.on(type, url, function);
pjax.on(type, function);
```
```javsctipy
// 通过配置监听
pjax.init({
    // ....
    events: {
        type: function(){}
    }
});
```

####事件类型
**init**<br>
在每个页面加载完成后触发,有一个object参数:{ title, html }

**end**<br>
在每个页面离开前触发

**ajaxBegin**<br>
在请求开始时触发。有一个object参数: { url, fnb, data, xhr }, url表示新页面的url,fnb表示是否由浏览器前进后退触发,data表示传到新页面的数据,xhr是请求的XMLHttpRequest()实例

**ajaxSuccess**<br>
在请求成功后触发。参数与begin一样。

**ajaxError**<br>
在请求失败后触发。参数与begin一样。


特性
---
* 优先使用标签上的data-coffce-pjax-href,其次使用href
* 标签上若有data-coffce-pjax属性,将作为data属性传递到新页面

```html
// 将跳转到b.html,并传递字符串data
<a href="a.html" data-coffce-pjax-href="b.html" data-coffce-pjax="data"></a>
```

服务端配合
---
* 对于PJAX请求,服务端并不需要返回完整的HTML,只返回变动的Content部分即可。对于普通请求(一般由浏览器地址栏直接打开),则需要返回完整的HTML。
* coffce-pjax在发送请求时,会带上请求头COFFCE-PJAX:true,你可以依此来判断当前请求是PJAX请求还是普通请求。
* 由于没有返回完整的HTML,服务端应该将document.title放在请求头COFFCE-PJAX-TITLE里。

注意:
------
作者很懒,没有认真测试过,接口也可能随时变动,使用需自己小心。

License
-----
MIT

================================================
FILE: demo/1.html
================================================
<p>1.html</p>

================================================
FILE: demo/2.html
================================================
<p>2.html</p>

================================================
FILE: demo/3.html
================================================
<p>3.html</p>

================================================
FILE: demo/index.html
================================================
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>COFFCE-PJAX-DEMO</title>
    <link rel="stylesheet" href="../styles/style.css">
</head>
<body>
    <div id="container"></div>
    <div id="main">
        <a href="1.html">1.html</a>
        <a href="2.html">2.html</a>
        <a href="3.html" data-coffce-pjax="传递到新页面的数据">3.html</a>
    </div>
    <!--Scripts-->
    <script src="../scripts/coffce-pjax.js"></script>
    <script>
        var container = document.getElementById("container");
        var pjax = CoffcePJAX;
        
        // 配置PJAX
        pjax.init({
            container: container,
            hash: true,
            filter: {
                content: function(title, html) {
                    // 已后退到首页,真实环境不需要这一步。
                    if (html.indexOf('<html lang="en">') > -1) {
                        container.innerHTML = "";
                        return false;
                    }

                    return true;
                }
            },
            events: {
                init: function() {
                    console.log("init");
                },
                end: function() {
                    console.log("end");  
                },
                ajaxBegin: function() {
                    console.log("ajaxBegin");
                },
                ajaxSuccess: function() {
                    console.log("ajaxSuccess");
                },
                ajaxError: function() {
                    console.log("ajaxError")
                }
            }
        });
    </script>
</body>
</html>

================================================
FILE: package.json
================================================
{
  "name": "coffce-pjax",
  "version": "0.0.4",
  "description": "A simple pjax library",
  "main": "scripts/coffce-pjax.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/Coffcer/Coffce-PJAX.git"
  },
  "keywords": [
    "pjax",
    "coffce"
  ],
  "author": "coffce",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/Coffcer/Coffce-PJAX/issues"
  },
  "homepage": "https://github.com/Coffcer/Coffce-PJAX"
}


================================================
FILE: scripts/coffce-pjax.js
================================================
/*jshint eqnull: true, expr: true, sub: true, browser: true, devel: true*/
/*global define, module */

/*!
 * Coffce-Pjax
 * 将页面所有的跳转替换为ajax请求,把网站改造成单页面应用
 * 兼容Chrome, Firefox, Safari, Android Browser, IE8+等
 * 在IE8和IE9上使用URL Hash,即地址栏的#号,你也可以选择不启用
 * 在更低版本的浏览器和搜索引擎蜘蛛上,保持默认跳转,不受影响
 */

(function (window, undefined) {
    "use strict";

    // 配置
    var config = {
        // 选择器,支持querySelector选择器
        selector: "a",
        // 要替换内容的容器,可为选择器字符串或DOM对象
        container: "body",
        // 是否在前进后退时开启本地缓存功能
        cache: true,
        // 是否对低版本浏览器启用hash方案
        hash: false,
        // 是否允许跳转到当前相同URL,相当于刷新
        same: true,
        // 调试模式,console.log调试信息
        debug: false,
        // 各个执行阶段的过滤函数,返回false则停止pjax执行
        filter: {
            // params: element
            // 选择器过滤,如果querySelector无法满足需求,可以在此函数里二次过滤
            selector: null,
            // params: title, html
            // 接收到ajax请求返回的内容时触发
            content: null
        },
        // 各个阶段的自定义函数,将替换默认实现
        custom: {
            // params: html, container
            // 自定义更换页面函数,可以在此实现动画效果等
            append: null
        },
        // 事件监听,合并到CoffcePJAX.on()里
        events: null
    };

    // 使用模式 枚举
    var SUPPORT = {
        // 不支持
        PASS: 0,
        // 使用Hash
        HASH: 1,
        // 使用HTML History API
        HTML5: 2
    };

    // 浏览器支持情况
    var suppost = history.pushState ? SUPPORT.HTML5 : ("onhashchange" in window ? SUPPORT.HASH : SUPPORT.PASS);

    var util = {
        /**
         * 合并两个对象,浅拷贝
         * @param {Object} obj1
         * @param {Object} obj2
         */
        extend: function (obj1, obj2) {
            if (!obj2) return;

            for (var key in obj2) {
                if (obj2.hasOwnProperty(key)) {
                    obj1[key] = obj2[key];
                }
            }

            return obj1;
        },
        /**
         * 输出调试信息,仅在config.debug为true时输出
         * @param {String} text
         */
        log: function (text) {
            config.debug && console.log("coffce-pjax: " + text);
        },
        /**
         * 获取url中的路径, 如:www.google.com/abcd 返回 /abcd
         * @param {String} url
         */
        getPath: function (url) {
            return url.replace(location.protocol + "//" + location.host, "");
        },
        /**
         * 通过相对路径获取完整的url
         * @param {String} href
         */
        getFullHref: function (href) {
            // 利用a标签来获取href,除此之外,a标签还能用来获取许多url相关信息
            var a = document.createElement("a");
            a.href = href;
            return a.href;
        },
        /**
         * 判断dom是否匹配选择器
         * @param {Object} element
         * @param {String} selector
         */
        matchSelector: function (element, selector) {
            var match =
                document.documentElement.webkitMatchesSelector ||
                document.documentElement.mozMatchesSelector ||
                document.documentElement.msMatchesSelector ||
                // 兼容IE8及以下浏览器
                function (selector, element) {
                    // 这是一个好方法,可惜IE8连indexOf都不支持
                    // return Array.prototype.indexOf.call(document.querySelectorAll(selector), this) !== -1;

                    if (element.tagName === selector.toUpperCase()) return true;

                    var elements = document.querySelectorAll(selector),
                        length = elements.length;

                    while (length--) {
                        if (elements[length] === this) return true;
                    }

                    return false;
                };

            // 重写函数自身,使用闭包keep住match函数,不用每次都判断兼容
            util.matchSelector = function (element, selector) {
                return match.call(element, selector);
            };

            return util.matchSelector(element, selector);
        }
    };

    var cache = {
        key: function (url) {
            return "coffce-pjax[" + url + "]";
        },
        get: function (url) {
            var value = sessionStorage.getItem(cache.key(url));
            return value && JSON.parse(value);
        },
        set: function (url, value) {
            // storage有容量上限,超出限额会报错
            try {
                sessionStorage.setItem(cache.key(url), JSON.stringify(value));
            } catch (e) {
                util.log("超出本地存储容量上线,本次操作将不使用本地缓存");
            }
        },
        clear: function () {
            var i = sessionStorage.length;
            while (i--) {
                var key = sessionStorage.key(i);
                if (key.indexOf("coffce-pjax") > -1) {
                    sessionStorage.removeItem(key);
                }
            }
        },
    };

    var event = {
        // 在浏览器前进后退时执行
        popstate: function () {
            core.fnb = true;
            core.turn(location.href, null, null);
        },
        // hash改变时执行,由于过滤了手动改变,所以也只在浏览器前进后退时执行
        hashchange: function () {
            if (!core.fnb) return;
            core.turn(location.href.replace("#/", ""), null, null);
        },
        click: function (e) {
            var element = e.target || e.srcElement;

            // 过滤不匹配选择器的元素
            if (!util.matchSelector(element, config.selector)) return;

            // 调用自定义过滤函数
            if (config.filter.selector && !config.filter.selector(element)) return;

            // 优先使用data-coffce-pjax-href
            var url = element.getAttribute("data-coffce-pjax-href");
            url = url ? util.getFullHref(url) : element.href;

            // 过滤空值
            if (url === undefined || url === "") return;

            // 阻止默认跳转,
            // 在这上面的return,仍会执行默认跳转,下面的就不会了
            e.preventDefault ? e.preventDefault() : (window.event.returnValue = false);

            // 阻止相同链接
            if (!config.same && url === location.href) return;

            // 标签上有这个值的话,将作为data传入新页面
            var data = element.getAttribute("data-coffce-pjax");

            core.fnb = false;
            core.turn(url, data, null);
        },
        bindEvent: function () {
            if (suppost === SUPPORT.HTML5) {
                window.addEventListener("popstate", event.popstate);
                window.addEventListener("click", event.click);
            } else {
                window.attachEvent("onhashchange", event.hashchange);
                document.documentElement.attachEvent("onclick", event.click);
            }
        },
        unbindEvent: function () {
            if (suppost === SUPPORT.HTML5) {
                window.removeEventListener("popstate", event.popstate);
                window.removeEventListener("click", event.click);
            } else {
                window.detachEvent("onhashchange", event.hashchange);
                document.documentElement.detachEvent("onclick", event.click);
            }
        }
    };

    var core = {
        // Forward And Back,表示当前操作是否由前进和后退触发
        fnb: false,
        // 显示新页面
        show: function (title, html) {
            pjax.trigger("end");

            document.title = title;

            if (config.custom.append) {
                config.custom.append(html, config.container);
            } else {
                config.container.innerHTML = html;
            }

            pjax.trigger("init");
        },
        // 跳转到指定页面
        turn: function (url, data, callback) {
            var eventData = {
                url: url,
                fnb: core.fnb,
                data: data
            };

            //pjax.trigger("begin", eventData);

            // 如果是由前进后退触发,并且开启了缓存,则试着从缓存中获取数据
            if (core.fnb && config.cache) {
                var value = cache.get(url);
                if (value !== null) {
                    core.show(value.title, value.html);
                    /*pjax.trigger("success", eventData);
                    pjax.trigger("end", eventData);*/
                    return;
                }
            }

            // 开始发送请求
            var xhr = new XMLHttpRequest();

            xhr.open("GET", url, true);
            xhr.setRequestHeader("COFFCE-PJAX", "true");
            xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");

            eventData.xhr = xhr;
            pjax.trigger("ajaxBegin", eventData);

            xhr.onreadystatechange = function () {
                if (xhr.readyState === 4) {
                    // 姑且认为200-300之间都是成功的请求,304是缓存
                    if (xhr.status >= 200 && xhr.status < 300 || xhr.status === 304) {
                        var title = xhr.getResponseHeader("COFFCE-PJAX-TITLE") || document.title,
                            html = xhr.responseText;

                        // 内容过滤器
                        if (config.filter.content && !config.filter.content(title, html)) {
                            util.log("filter.content过滤不通过");
                        } else {
                            callback && callback(data);
                            pjax.trigger("ajaxSuccess", eventData);
                            
                            // 显示新页面
                            core.show(title, html);

                            if (!core.fnb) {
                                // 修改URL
                                if (suppost === SUPPORT.HTML5) {
                                    history.pushState(null, null, url);
                                } else {
                                    location.hash = util.getPath(url);
                                }

                                // 添加到缓存
                                if (config.cache) {
                                    cache.set(url, {
                                        title: title,
                                        html: html
                                    });
                                }
                            }
                        }
                    } else {
                        pjax.trigger("ajaxError", null, eventData);
                        util.log("请求失败,错误码:" + xhr.status);
                    }

                    core.fnb = true;
                }
            };
            xhr.send();
        }
    };

    var pjax = {
        ready: false,
        events: {},
        /**
         * 初始化
         * @param {Object} options 配置
         */
        init: function (options) {
            if (suppost === SUPPORT.PASS) {
                util.log("不支持该版本的浏览器");
                return;
            }

            util.extend(config, options);

            // 将config.container转换为dom
            if (typeof config.container === "string") {
                var selectorName = config.container;

                config.container = document.querySelector(config.container);
                if (config.container === null) {
                    throw new Error("找不到Element:" + selectorName);
                }
            }

            // 监听配置里的事件
            if (config.events) {
                for (var key in config.events) {
                    pjax.on(key, null, config.events[key]);
                }
            }

            // 如果一打开就已经带有hash, 则立刻发请求
            // 由于hash不会被传到服务器,此时页面多半是首页,如打开www.google.com/#/abcd,其实是打开了www.google.com
            if (suppost === SUPPORT.HASH && location.hash.length > 2) {
                // 先删了当前内容,防止用户误会
                config.container.innerHTML = "";
                pjax.ready = true;

                core.fnd = false;
                core.turn(location.href.replace("#/", ""), null, function () {
                    pjax.trigger("init");
                });
            }

            event.bindEvent();

            if (!pjax.ready) {
                pjax.ready = true;
                pjax.trigger("init");
            }
        },
        // 注销插件,一般来说你并不需要使用这个方法
        destroy: function () {
            pjax.events = null;
            event.unbindEvent();
            util.clearCache();
        },
        /**
         * 使用pjax跳转到指定页面
         * @param {String}   url
         * @param {Object}   data     要传到新页面的参数,可以为null
         * @param {Function} callback 请求成功时的回调
         */
        turn: function (url, data, callback) {
            url = util.getFullHref(url);
            core.fnb = false;
            core.turn(url, data, callback);
        },
        /**
         * 监听事件
         * @param {String}   type     事件类型
         * @param {String}   url      指定监听该事件的页面,null表示所有页面都监听
         * @param {Function} listener 回调
         */
        on: function (type, url, listener) {
            // 只有两个参数,跳过中间的url
            if (listener === undefined) {
                listener = url;
                url = null;
            } else if (url) {
                url = util.getFullHref(url);
            }

            pjax.events[type] = pjax.events[type] || [];
            pjax.events[type].push({
                listener: listener,
                url: url
            });
        },
        /**
         * 解除监听
         * @param {String} type 事件类型
         * @param {String} url 解绑该事件的页面,null表示所有页面都解绑
         */
        off: function (type, url) {
            if (url) {
                var list = pjax.events[type];
                url = util.getFullHref(url);

                for (var i = 0; i < list.length; i++) {
                    if (list[i].url === url) {
                        list.splice(i, 1);
                        i--;
                    }
                }

                if (list.length) return;
            }

            delete pjax.events[type];
        },
        /**
         * 触发事件
         * @param {String} type 事件类型
         * @param {Object} args 参数
         */
        trigger: function (type, args) {
            var list = pjax.events[type];
            if (list) {
                for (var i = 0, length = list.length; i < length; i++) {
                    list[i].listener.call(pjax, args);
                }
            }
        }
    };

    if (typeof define === "function" && define.amd) {
        define([], function () {
            return pjax;
        });
    } else if (typeof module === "object" && typeof exports === "object") {
        module.exports = pjax;
    } else {
        window.CoffcePJAX = pjax;
    }

})(window);

================================================
FILE: styles/style.css
================================================
* {
    box-sizing: border-box;
}
html {
    height: 100%;
    background-color: #EFEFEF;
}
body {
    margin: 0;
    height: 100%;
    overflow: hidden;
}
#container {
    position: relative;
    height: 50%;
    border-bottom: solid 1px #C1C1C1;
}
#container p {
    position: absolute;
    top: 0;
    bottom: 0;
    margin: auto;
    height: 40px;
    width: 100%;
    color: #555;
    text-align: center;
    font: 30px "microsoft yahei";
    transition: all .3s;
}
#main {
    padding: 40px;
    height: 50%;
    text-align: center;
    border-top: solid 1px #FFF;
}
#main a {
    margin: 0 20px;
    color: #1075BF;
    text-decoration: none;
    font-family: "microsoft yahei";
}
Download .txt
gitextract_aaoaea37/

├── .brackets.json
├── .gitattributes
├── README.md
├── demo/
│   ├── 1.html
│   ├── 2.html
│   ├── 3.html
│   └── index.html
├── package.json
├── scripts/
│   └── coffce-pjax.js
└── styles/
    └── style.css
Condensed preview — 10 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (26K chars).
[
  {
    "path": ".brackets.json",
    "chars": 65,
    "preview": "{\n    \"sbruchmann.staticpreview.basepath\": \"G:/轮子/coffce-pjax/\"\n}"
  },
  {
    "path": ".gitattributes",
    "chars": 378,
    "preview": "# Auto detect text files and perform LF normalization\n* text=auto\n\n# Custom for Visual Studio\n*.cs     diff=csharp\n\n# St"
  },
  {
    "path": "README.md",
    "chars": 3357,
    "preview": "coffce-pjax\n===\ncoffce-pjax可以将页面所有的跳转替换为AJAX请求,把网站改造成单页面应用。<br>\nnote: 由于浏览器限制,pjax需要在服务器环境下使用,即不要使用file://xxx.html运行。\n\n#"
  },
  {
    "path": "demo/1.html",
    "chars": 13,
    "preview": "<p>1.html</p>"
  },
  {
    "path": "demo/2.html",
    "chars": 13,
    "preview": "<p>2.html</p>"
  },
  {
    "path": "demo/3.html",
    "chars": 13,
    "preview": "<p>3.html</p>"
  },
  {
    "path": "demo/index.html",
    "chars": 1595,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <title>COFFCE-PJAX-DEMO</title>\n    <link rel=\"st"
  },
  {
    "path": "package.json",
    "chars": 524,
    "preview": "{\n  \"name\": \"coffce-pjax\",\n  \"version\": \"0.0.4\",\n  \"description\": \"A simple pjax library\",\n  \"main\": \"scripts/coffce-pja"
  },
  {
    "path": "scripts/coffce-pjax.js",
    "chars": 14195,
    "preview": "/*jshint eqnull: true, expr: true, sub: true, browser: true, devel: true*/\n/*global define, module */\n\n/*!\n * Coffce-Pja"
  },
  {
    "path": "styles/style.css",
    "chars": 687,
    "preview": "* {\n    box-sizing: border-box;\n}\nhtml {\n    height: 100%;\n    background-color: #EFEFEF;\n}\nbody {\n    margin: 0;\n    he"
  }
]

About this extraction

This page contains the full source code of the Coffcer/coffce-pjax GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 10 files (20.4 KB), approximately 6.0k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!