├── .github
└── workflows
│ ├── binary-build.yml
│ ├── docker-publish.yml
│ └── notifications.yml
├── .gitignore
├── Dockerfile
├── LICENSE
├── README.md
├── assets
├── ddark.png
├── dlight.png
├── dmddark.png
├── dmdlight.png
├── dmdrdark.png
├── dmdrlight.png
├── drtextdark.png
├── drtextlight.png
├── dsnippetdark.png
├── dsnippetlight.png
├── logo.png
├── logo.svg
├── mdark.png
├── mlight.png
├── mmddark.png
├── mmdlight.png
├── mmdrdark.png
├── mmdrlight.png
├── mrtextdark.png
├── mrtextlight.png
├── msnippetdark.png
└── msnippetlight.png
├── go.mod
├── go.sum
├── main.go
├── scripts
└── asset-download.sh
├── static
├── css
│ ├── catppuccin-latte.css
│ ├── catppuccin-mocha.css
│ └── inter.css
├── favicon.ico
├── fontawesome
│ ├── css
│ │ └── all.min.css
│ └── webfonts
│ │ ├── fa-brands-400.woff2
│ │ ├── fa-regular-400.woff2
│ │ ├── fa-solid-900.woff2
│ │ └── fa-v4compatibility.woff2
├── fonts
│ ├── UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuDyYMZg.ttf
│ ├── UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuFuYMZg.ttf
│ ├── UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuGKYMZg.ttf
│ ├── UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuI6fMZg.ttf
│ └── UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfMZg.ttf
├── icon-192.png
├── icon-512.png
├── js
│ ├── highlight.min.js
│ └── marked.min.js
├── manifest.json
├── md.js
├── rtext.js
├── style.css
└── sw.js
└── templates
├── index.html
├── md.html
├── rtext.html
└── show.html
/.github/workflows/binary-build.yml:
--------------------------------------------------------------------------------
1 | name: Build Binary
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 |
7 | permissions:
8 | contents: write
9 | packages: write
10 |
11 | jobs:
12 | check-commit:
13 | runs-on: ubuntu-latest
14 | outputs:
15 | version: ${{ steps.version.outputs.new_version }}
16 | steps:
17 | - uses: actions/checkout@v4
18 | with:
19 | fetch-depth: 0
20 |
21 | - name: Determine Version
22 | id: version
23 | run: |
24 | # Get the latest version tag, default to v0 if none exists
25 | LATEST_TAG=$(gh release list -L 1 | cut -f 1 | sed 's/Release //' || echo "v0")
26 | LATEST_TAG=${LATEST_TAG:-v0}
27 |
28 | # Extract current version numbers
29 | VERSION=$(echo $LATEST_TAG | sed 's/v//')
30 |
31 | NEW_VERSION="v$((VERSION + 1))"
32 |
33 | echo "Previous version: $LATEST_TAG"
34 | echo "New version: $NEW_VERSION"
35 | echo "new_version=$NEW_VERSION" >> "$GITHUB_OUTPUT"
36 | env:
37 | GH_TOKEN: ${{ github.token }}
38 |
39 | - name: Create Release
40 | run: |
41 | gh release create "${{ steps.version.outputs.new_version }}" \
42 | --title "Release ${{ steps.version.outputs.new_version }}" \
43 | --draft \
44 | --notes "Local Content Share latest release binaries." \
45 | --target ${{ github.sha }}
46 | env:
47 | GH_TOKEN: ${{ github.token }}
48 |
49 | build:
50 | needs: check-commit
51 | runs-on: ubuntu-latest
52 | strategy:
53 | matrix:
54 | os: [linux, windows, darwin]
55 | arch: [amd64, arm64]
56 | steps:
57 | - uses: actions/checkout@v4
58 |
59 | - name: Set up Go
60 | uses: actions/setup-go@v5
61 | with:
62 | go-version: '1.23'
63 |
64 | - name: Build Binary
65 | run: |
66 | GOOS=${{ matrix.os }} GOARCH=${{ matrix.arch }} go build -ldflags="-s -w" -o local-content-share-${{ matrix.os }}-${{ matrix.arch }}${{ matrix.os == 'windows' && '.exe' || '' }}
67 |
68 | - name: Upload Release Asset
69 | run: |
70 | gh release upload "${{ needs.check-commit.outputs.version }}" \
71 | "local-content-share-${{ matrix.os }}-${{ matrix.arch }}${{ matrix.os == 'windows' && '.exe' || '' }}"
72 | env:
73 | GH_TOKEN: ${{ github.token }}
74 |
75 | publish:
76 | needs: [check-commit, build]
77 | runs-on: ubuntu-latest
78 | steps:
79 | - uses: actions/checkout@v4
80 |
81 | - name: Publish Release
82 | run: |
83 | gh release edit "${{ needs.check-commit.outputs.version }}" --draft=false
84 | env:
85 | GH_TOKEN: ${{ github.token }}
86 |
--------------------------------------------------------------------------------
/.github/workflows/docker-publish.yml:
--------------------------------------------------------------------------------
1 | name: Docker Publish
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 |
7 | jobs:
8 | publish:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v4
12 |
13 | - name: Set up QEMU
14 | uses: docker/setup-qemu-action@v3
15 |
16 | - name: Set up Docker Buildx
17 | uses: docker/setup-buildx-action@v3
18 |
19 | - name: Login to Docker Hub
20 | uses: docker/login-action@v3
21 | with:
22 | username: tanq16
23 | password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
24 |
25 | - name: Build and push Docker image
26 | uses: docker/build-push-action@v5
27 | with:
28 | context: .
29 | platforms: linux/amd64,linux/arm64
30 | push: true
31 | tags: tanq16/local-content-share:main
32 |
--------------------------------------------------------------------------------
/.github/workflows/notifications.yml:
--------------------------------------------------------------------------------
1 | name: Custom Notifications
2 | on:
3 | schedule:
4 | - cron: '30 15 * * 6' # 3:30 pm UTC every saturday
5 | issues:
6 | types: [opened, edited, deleted, closed]
7 | issue_comment:
8 | types: [created]
9 | workflow_run:
10 | workflows: ["Docker Publish", "Build Binary"]
11 | types: [completed]
12 | pull_request_target:
13 | types: [opened, closed, edited, review_requested]
14 |
15 | jobs:
16 | weekly-summary:
17 | if: github.event_name == 'schedule'
18 | runs-on: ubuntu-latest
19 | steps:
20 | - name: Calculate Summary
21 | run: |
22 | REPO="${{ github.repository }}"
23 | STARS=$(curl -s -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" "https://api.github.com/repos/$REPO" | jq .stargazers_count)
24 | FORKS=$(curl -s -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" "https://api.github.com/repos/$REPO" | jq .forks_count)
25 | COMMITS=$(curl -s -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
26 | "https://api.github.com/repos/$REPO/commits?since=$(date -u -d 'last saturday' '+%Y-%m-%dT%H:%M:%SZ')" | jq length)
27 | curl -H "Content-Type: application/json" -X POST \
28 | -d "{\"content\": \"*Weekly summary for **$REPO***\nStars - $STARS, Forks - $FORKS, Commits this week - $COMMITS\"}" ${{ secrets.DISCORD_WEBHOOK }}
29 |
30 | issue-comment-notification:
31 | if: github.event_name == 'issues' || github.event_name == 'issue_comment'
32 | runs-on: ubuntu-latest
33 | steps:
34 | - name: Notify on Issue or Comment
35 | if: github.actor != 'Tanq16'
36 | run: |
37 | curl -H "Content-Type: application/json" -X POST \
38 | -d "{\"content\": \"*New issue/comment from **${{ github.actor }}***\n${{ github.event.issue.html_url }}\"}" ${{ secrets.DISCORD_WEBHOOK }}
39 |
40 | build-status-notification:
41 | if: github.event_name == 'workflow_run'
42 | runs-on: ubuntu-latest
43 | steps:
44 | - name: Notify on Build Status
45 | run: |
46 | curl -H "Content-Type: application/json" -X POST \
47 | -d "{\"content\": \"*Workflow run for **${{ github.repository }}***\n${{ github.event.workflow_run.name }} - ${{ github.event.workflow_run.conclusion }}\"}" ${{ secrets.DISCORD_WEBHOOK }}
48 |
49 | pull-request-notification:
50 | if: github.event_name == 'pull_request_target'
51 | runs-on: ubuntu-latest
52 | steps:
53 | - name: Notify on PR related activities
54 | if: github.actor != 'Tanq16'
55 | run: |
56 | curl -H "Content-Type: application/json" -X POST \
57 | -d "{\"content\": \"*New PR activity from **${{ github.actor }}***\n${{ github.event.pull_request.html_url }}\"}" ${{ secrets.DISCORD_WEBHOOK }}
58 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | data
2 | local-content-share
3 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:alpine AS builder
2 |
3 | WORKDIR /app
4 |
5 | COPY . .
6 |
7 | # Build the application
8 | RUN go build -ldflags="-s -w" -o local-content-share .
9 |
10 | # Use a minimal alpine image for running
11 | FROM alpine:latest
12 |
13 | WORKDIR /app
14 |
15 | # Create data directory if not exists
16 | RUN mkdir -p /app/data
17 |
18 | # Copy the binary from builder
19 | COPY --from=builder /app/local-content-share .
20 |
21 | # Expose the default port
22 | EXPOSE 8080
23 |
24 | # Run the server
25 | CMD ["/app/local-content-share"]
26 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Tanishq Rupaal
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 |
9 |
10 | ---
11 |
12 | A simple & elegant self-hosted app for **storing/sharing text snippets and files** in your **local network** with **no setup on client devices**. Think of this as an *all-in-one alternative* to **airdrop**, **local-pastebin**, and a **scratchpad**. The primary features are:
13 |
14 | - Make plain text **snippets** available to **view/share** on any device in the local network
15 | - **Upload files** and make them available to **view/download** on any device in the local network
16 | - Built-in **Notepad** with both **Markdown** and **Rich Text** editing capabilities
17 | - **Rename** text snippets and files uploaded to easily find them in the UI
18 | - **Edit** text snippets to modify their content as needed
19 | - **Multi-file** **drag-n-drop** (drop into the text area) support for uploading files
20 | - Configurable **expiration (or TTL, i.e., time to live)** per file/snippet for Never, 1 hour, 4 hours, 1 day, or Custom
21 | - Use of **SSE** to automatically inform all clients of new/deleted/edited files
22 | - Completely **local assets**, so the app works in your network even without internet
23 | - **Multi-arch** (x86-64 and ARM64) **Docker image** for **homelab** deployments
24 | - Frontend available over **browsers** and as a **PWA** (progressive web apps)
25 | - Clean, modern interface with **automatic light/dark** UI that looks good on mobile too
26 |
27 | Make sure to look into [Tips & Notes](#tips-and-notes) if you have questions about individual functionalities.
28 |
29 | > [!NOTE]
30 | > This application is meant to be deployed within your homelab only. There is no authentication mechanism implemented. If you are exposing to the public, ensure there is authentication fronting it and non-destructive users using it.
31 |
32 | ## Screenshots
33 |
34 | | | Desktop View | Mobile View |
35 | | --- | --- | --- |
36 | | Light | | |
37 | | Dark | | |
38 |
39 |
40 | Expand for more screenshots
41 |
42 | | Desktop View | Mobile View |
43 | | --- | --- |
44 | | | |
45 | | | |
46 | | | |
47 | | | |
48 | | | |
49 | | | |
50 | | | |
51 | | | |
52 |
53 |
54 |
55 | ## Installation and Usage
56 |
57 | ### Using Docker (Recommended for Self-Hosting)
58 |
59 | Use `docker` CLI one liner and setup a persistence directory (so a container failure does not delete your data):
60 |
61 | ```bash
62 | mkdir $HOME/.localcontentshare
63 | ```
64 | ```bash
65 | docker run --name local-content-share \
66 | -p 8080:8080 \
67 | -v $HOME/.localcontentshare:/app/data \
68 | tanq16/local-content-share:main
69 | ```
70 |
71 | The application will be available at `http://localhost:8080` (or your server IP).
72 |
73 | You can also use the following compose file with container managers like Portainer and Dockge (remember to change the mounted volume):
74 |
75 | ```yaml
76 | services:
77 | contentshare:
78 | image: tanq16/local-content-share:main
79 | container_name: local-content-share
80 | volumes:
81 | - /home/tanq/lcshare:/app/data # Change as needed
82 | ports:
83 | - 8080:8080
84 | ```
85 |
86 | ### Using Binary
87 |
88 | Download the appropriate binary for your system from the [latest release](https://github.com/tanq16/local-content-share/releases/latest).
89 |
90 | Make the binary executable (for Linux/macOS) with `chmod +x local-content-share-*` and then run the binary with `./local-content-share-*`. The application will be available at `http://localhost:8080`.
91 |
92 | ### Local development
93 |
94 | With `Go 1.23+` installed, run the following to download the binary to your GOBIN:
95 |
96 | ```bash
97 | go install github.com/tanq16/local-content-share@latest
98 | ```
99 |
100 | Or, you can build from source like so:
101 |
102 | ```bash
103 | git clone https://github.com/tanq16/local-content-share.git && \
104 | cd local-content-share && \
105 | go build .
106 | ```
107 |
108 | ## Tips and Notes
109 |
110 | - To share text content:
111 | - Type or paste your text in the text area (the upload button will change to a submit button)
112 | - (OPTIONAL) type the name of the snippet (otherwise it will name it as a time string)
113 | - Click the submit button (looks like the telegram arrow) to upload the snippet
114 | - To rename files or text snippets:
115 | - Click the cursor (i-beam) icon and provide the new name
116 | - It will automatically prepend 4 random digits if the name isn't unique
117 | - To edit existing snippets:
118 | - Click the pen icon and it will populate the expandable text area with the content
119 | - Write the new content and click accept or deny (check or cross) in the same text area
120 | - On accepting, it will edit the content; on denying, it will refresh the page
121 | - To share files:
122 | - Click the upload button and select your file
123 | - OR drag and drop your file (even multiple files) to the text area
124 | - It will automatically append 4 random digits if filename isn't unique
125 | - To view content, click the eye icon:
126 | - For text content, it shows the raw text, which can be copied with a button on top
127 | - For files, it shows raw text, images, PDFs, etc. (basically whatever the browser will do)
128 | - To download files, click the download icon
129 | - To delete content, click the trash icon
130 | - To set expiration for a file or snippet
131 | - Click the clock icon with the "Never" text (signifying no expiry) to cycle between times
132 | - For a non-"Never" expiration, the file will automatically be removed after the specified period
133 | - Set the cycling button to 1 hour, 4 hours, 1 day, or Custom before adding a snippet or file
134 | - The Custom option will prompt to ask for the expiry after you click submit/upload
135 | - The value for custom expiration can be of the format `NT` (eg. `34m`, `3w`, `2M`, `11d`)
136 | - N is the number and T is the time denomination (m=minute, h=hour, d=day, w=week, M=month, y=year)
137 | - Use the `DEFAULT_EXPIRY` environment variable to set a default expiration (follows format of Custom specified above)
138 | - This value will be set as default on the home page instead of `Never`
139 | - The other options will still be available by cycling if needed
140 | - The Notepad is for writing something quickly and getting back to it from any device
141 | - It supports both markdown and richtext modes
142 | - Content is automatically saved upon inactivity in the backend and will load as is on any device
143 |
144 | ### A Note on Reverse Proxies
145 |
146 | Reverse proxies are fairly common in homelab settings to assign SSL certificates and use domains. The reason for this note is that some reverse proxy settings may interfere with the functioning of this app. Primarily, there are 2 features that could be affected:
147 |
148 | - File Size: reverse proxy software may impose a limit on file sizes, but Local Content Share does not
149 | - Upload Progress: file upload progress for large files may not be visible until the file has been uploaded because of buffering setups on rever proxy software
150 |
151 | Following is a sample fix for Nginx Proxy Manager, please look into equivalent settings for other reverse proxy setups like Caddy.
152 |
153 | For the associated proxy host in NPM, click Edit and visit the Advanced tab. There, paste the following custom configuration:
154 |
155 | ```
156 | client_max_body_size 5G;
157 | proxy_request_buffering off;
158 | ```
159 |
160 | This configuration will set the maximum accept size for file transfer through NPM as 5 GB and will also disable buffering so interaction will take place directly with Local Content Share.
161 |
162 | ### Backend Data Structure
163 |
164 | The application creates a `data` directory to store all uploaded files, uploaded text snippets, notepad notes (in `files`, `text`, and `notepad` subfolders respectively). File expirations are saved in an `expiration.json` file in the data directory. Make sure the application has write permissions for the directory where it runs.
165 |
166 | ## Acknowledgements
167 |
168 | The following people have contributed to the project:
169 |
170 | - [TheArktect](https://github.com/TheArktect) - Added CLI argument for listen address.
171 | - A lot of other users who created feature requests via GitHub issues.
172 |
--------------------------------------------------------------------------------
/assets/ddark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tanq16/local-content-share/00ad44b8d9b6999872ad97b510343ba858a76de4/assets/ddark.png
--------------------------------------------------------------------------------
/assets/dlight.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tanq16/local-content-share/00ad44b8d9b6999872ad97b510343ba858a76de4/assets/dlight.png
--------------------------------------------------------------------------------
/assets/dmddark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tanq16/local-content-share/00ad44b8d9b6999872ad97b510343ba858a76de4/assets/dmddark.png
--------------------------------------------------------------------------------
/assets/dmdlight.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tanq16/local-content-share/00ad44b8d9b6999872ad97b510343ba858a76de4/assets/dmdlight.png
--------------------------------------------------------------------------------
/assets/dmdrdark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tanq16/local-content-share/00ad44b8d9b6999872ad97b510343ba858a76de4/assets/dmdrdark.png
--------------------------------------------------------------------------------
/assets/dmdrlight.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tanq16/local-content-share/00ad44b8d9b6999872ad97b510343ba858a76de4/assets/dmdrlight.png
--------------------------------------------------------------------------------
/assets/drtextdark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tanq16/local-content-share/00ad44b8d9b6999872ad97b510343ba858a76de4/assets/drtextdark.png
--------------------------------------------------------------------------------
/assets/drtextlight.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tanq16/local-content-share/00ad44b8d9b6999872ad97b510343ba858a76de4/assets/drtextlight.png
--------------------------------------------------------------------------------
/assets/dsnippetdark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tanq16/local-content-share/00ad44b8d9b6999872ad97b510343ba858a76de4/assets/dsnippetdark.png
--------------------------------------------------------------------------------
/assets/dsnippetlight.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tanq16/local-content-share/00ad44b8d9b6999872ad97b510343ba858a76de4/assets/dsnippetlight.png
--------------------------------------------------------------------------------
/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tanq16/local-content-share/00ad44b8d9b6999872ad97b510343ba858a76de4/assets/logo.png
--------------------------------------------------------------------------------
/assets/mdark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tanq16/local-content-share/00ad44b8d9b6999872ad97b510343ba858a76de4/assets/mdark.png
--------------------------------------------------------------------------------
/assets/mlight.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tanq16/local-content-share/00ad44b8d9b6999872ad97b510343ba858a76de4/assets/mlight.png
--------------------------------------------------------------------------------
/assets/mmddark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tanq16/local-content-share/00ad44b8d9b6999872ad97b510343ba858a76de4/assets/mmddark.png
--------------------------------------------------------------------------------
/assets/mmdlight.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tanq16/local-content-share/00ad44b8d9b6999872ad97b510343ba858a76de4/assets/mmdlight.png
--------------------------------------------------------------------------------
/assets/mmdrdark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tanq16/local-content-share/00ad44b8d9b6999872ad97b510343ba858a76de4/assets/mmdrdark.png
--------------------------------------------------------------------------------
/assets/mmdrlight.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tanq16/local-content-share/00ad44b8d9b6999872ad97b510343ba858a76de4/assets/mmdrlight.png
--------------------------------------------------------------------------------
/assets/mrtextdark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tanq16/local-content-share/00ad44b8d9b6999872ad97b510343ba858a76de4/assets/mrtextdark.png
--------------------------------------------------------------------------------
/assets/mrtextlight.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tanq16/local-content-share/00ad44b8d9b6999872ad97b510343ba858a76de4/assets/mrtextlight.png
--------------------------------------------------------------------------------
/assets/msnippetdark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tanq16/local-content-share/00ad44b8d9b6999872ad97b510343ba858a76de4/assets/msnippetdark.png
--------------------------------------------------------------------------------
/assets/msnippetlight.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tanq16/local-content-share/00ad44b8d9b6999872ad97b510343ba858a76de4/assets/msnippetlight.png
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/tanq16/local-content-share
2 |
3 | go 1.23.2
4 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tanq16/local-content-share/00ad44b8d9b6999872ad97b510343ba858a76de4/go.sum
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "embed"
5 | "encoding/json"
6 | "flag"
7 | "fmt"
8 | "html/template"
9 | "io"
10 | "io/fs"
11 | "log"
12 | "math/rand"
13 | "net/http"
14 | "os"
15 | "path/filepath"
16 | "regexp"
17 | "strconv"
18 | "strings"
19 | "sync"
20 | "time"
21 | )
22 |
23 | //go:embed templates/* static/*
24 | var content embed.FS
25 |
26 | // SSE client management
27 | var (
28 | clients = make(map[chan string]bool)
29 | clientMux sync.Mutex
30 | )
31 |
32 | type Entry struct {
33 | ID string
34 | Content string
35 | Type string
36 | Filename string
37 | }
38 |
39 | type ExpirationTracker struct {
40 | Expirations map[string]time.Time `json:"expirations"`
41 | mu sync.Mutex // mutex for thread safety
42 | }
43 |
44 | var expirationTracker *ExpirationTracker
45 | var expirationOptions = []string{"Never", "1 hour", "4 hours", "1 day", "Custom"}
46 |
47 | func initExpirationTracker() *ExpirationTracker {
48 | tracker := &ExpirationTracker{
49 | Expirations: make(map[string]time.Time),
50 | }
51 | // Load existing expirations from file
52 | expirationFile := filepath.Join("data", "expirations.json")
53 | if _, err := os.Stat(expirationFile); err == nil {
54 | data, err := os.ReadFile(expirationFile)
55 | if err == nil {
56 | var storedTracker ExpirationTracker
57 | if err := json.Unmarshal(data, &storedTracker); err == nil {
58 | tracker.Expirations = storedTracker.Expirations
59 | }
60 | }
61 | }
62 | return tracker
63 | }
64 |
65 | func parseCustomDuration(customExpiry string) time.Duration {
66 | customExpiry = strings.TrimSpace(customExpiry)
67 | // Regex to match the format like 1h, 30m, 2d, etc.
68 | re := regexp.MustCompile(`^(\d+)([hmMdwy])$`)
69 | matches := re.FindStringSubmatch(customExpiry)
70 | if len(matches) < 2 { // bad value
71 | return 5 * time.Minute
72 | }
73 | value, err := strconv.Atoi(matches[1])
74 | if err != nil {
75 | return 5 * time.Minute
76 | }
77 | unit := strings.ToLower(matches[2])
78 | switch unit {
79 | case "m": // minutes
80 | if value < 5 {
81 | return 5 * time.Minute
82 | }
83 | return time.Duration(value) * time.Minute
84 | case "h": // hours
85 | return time.Duration(value) * time.Hour
86 | case "d": // days
87 | return time.Duration(value) * 24 * time.Hour
88 | case "w": // weeks
89 | return time.Duration(value) * 7 * 24 * time.Hour
90 | case "M": // months
91 | return time.Duration(value) * 30 * 24 * time.Hour
92 | case "y": // years
93 | return time.Duration(value) * 365 * 24 * time.Hour
94 | default:
95 | return 5 * time.Minute
96 | }
97 | }
98 |
99 | func (t *ExpirationTracker) SetExpiration(fileID, expiryOption string) {
100 | t.mu.Lock()
101 | defer t.mu.Unlock()
102 | if expiryOption == "Never" {
103 | delete(t.Expirations, fileID)
104 | } else {
105 | var duration time.Duration
106 | switch expiryOption {
107 | case "1 hour":
108 | duration = 1 * time.Hour
109 | case "4 hours":
110 | duration = 4 * time.Hour
111 | case "1 day":
112 | duration = 24 * time.Hour
113 | case "Custom":
114 | // Should not happen anymore.
115 | return
116 | default:
117 | if len(expiryOption) > 0 {
118 | duration = parseCustomDuration(expiryOption)
119 | } else {
120 | delete(t.Expirations, fileID)
121 | return
122 | }
123 | }
124 | t.Expirations[fileID] = time.Now().Add(duration)
125 | }
126 | t.saveToFile()
127 | }
128 |
129 | func (t *ExpirationTracker) saveToFile() {
130 | data, err := json.MarshalIndent(t, "", " ")
131 | if err != nil {
132 | log.Printf("Error marshaling expirations: %v", err)
133 | return
134 | }
135 | expirationFile := filepath.Join("data", "expirations.json")
136 | if err := os.WriteFile(expirationFile, data, 0644); err != nil {
137 | log.Printf("Error saving expirations: %v", err)
138 | }
139 | }
140 |
141 | func (t *ExpirationTracker) CleanupExpired() []string {
142 | t.mu.Lock()
143 | defer t.mu.Unlock()
144 | now := time.Now()
145 | var expiredFiles []string
146 | // Find expired files
147 | for fileID, expiryTime := range t.Expirations {
148 | if now.After(expiryTime) {
149 | expiredFiles = append(expiredFiles, fileID)
150 | }
151 | }
152 | // Delete expired files
153 | for _, fileID := range expiredFiles {
154 | err := os.Remove(filepath.Join("data", fileID))
155 | if err != nil && !os.IsNotExist(err) {
156 | log.Printf("Error removing expired file %s: %v", fileID, err)
157 | } else {
158 | log.Printf("Removed expired file: %s", fileID)
159 | }
160 | delete(t.Expirations, fileID)
161 | }
162 | if len(expiredFiles) > 0 {
163 | t.saveToFile()
164 | notifyContentChange()
165 | }
166 | return expiredFiles
167 | }
168 |
169 | var listenAddress = flag.String("listen", ":8080", "host:port in which the server will listen")
170 |
171 | // Placeholder content for notepad files
172 | const mdPlaceholder = `# Welcome to Markdown Notepad
173 |
174 | Start typing your markdown here...
175 |
176 | ## Features
177 |
178 | - **Bold** and *italic* text
179 | - [Links](https://example.com)
180 | - Lists (ordered and unordered)
181 | - Code blocks
182 | - And more!
183 |
184 | ` + "```" + `
185 | function example() {
186 | console.log("Hello, Markdown!");
187 | }
188 | ` + "```"
189 |
190 | const rtextPlaceholder = `Welcome to Rich Text Notepad
191 | Start typing here to create your document. Use the toolbar above to format your text.
`
192 |
193 | func generateUniqueFilename(baseDir, baseName string) string {
194 | // Sanitize: allow only letters, numbers, hyphen, underscore, and space
195 | reg := regexp.MustCompile(`[^\p{L}\p{N}\p{M}\s\.\-_]`)
196 | sanitizedName := reg.ReplaceAllString(baseName, "-")
197 | log.Printf("Sanitized name %s TO %s\n", baseName, sanitizedName)
198 | // First try without random prefix
199 | if _, err := os.Stat(filepath.Join(baseDir, sanitizedName)); os.IsNotExist(err) {
200 | return sanitizedName
201 | }
202 | // If file exists, add random prefix until we find a unique name
203 | for {
204 | randChars := fmt.Sprintf("%04d", rand.Intn(10000))
205 | newName := fmt.Sprintf("%s-%s", randChars, sanitizedName)
206 | if _, err := os.Stat(filepath.Join(baseDir, newName)); os.IsNotExist(err) {
207 | return newName
208 | }
209 | }
210 | }
211 |
212 | func handleContentUpdates(w http.ResponseWriter, r *http.Request) {
213 | w.Header().Set("Content-Type", "text/event-stream")
214 | w.Header().Set("Cache-Control", "no-cache")
215 | w.Header().Set("Connection", "keep-alive")
216 | w.Header().Set("Access-Control-Allow-Origin", "*")
217 |
218 | messageChan := make(chan string)
219 | clientMux.Lock()
220 | clients[messageChan] = true
221 | clientMux.Unlock()
222 |
223 | defer func() {
224 | clientMux.Lock()
225 | delete(clients, messageChan)
226 | clientMux.Unlock()
227 | close(messageChan)
228 | }()
229 | // Send an initial message
230 | fmt.Fprintf(w, "data: %s\n\n", "connected")
231 | w.(http.Flusher).Flush()
232 | for {
233 | select {
234 | case <-r.Context().Done():
235 | return
236 | case msg := <-messageChan:
237 | fmt.Fprintf(w, "data: %s\n\n", msg)
238 | w.(http.Flusher).Flush()
239 | }
240 | }
241 | }
242 |
243 | func notifyContentChange() {
244 | clientMux.Lock()
245 | defer clientMux.Unlock()
246 | for client := range clients {
247 | select {
248 | case client <- "content_updated":
249 | default:
250 | }
251 | }
252 | }
253 |
254 | func main() {
255 | flag.Parse()
256 |
257 | if err := os.MkdirAll(filepath.Join("data", "files"), 0755); err != nil {
258 | log.Fatal(err)
259 | }
260 | if err := os.MkdirAll(filepath.Join("data", "text"), 0755); err != nil {
261 | log.Fatal(err)
262 | }
263 | // Create notepad directory
264 | if err := os.MkdirAll(filepath.Join("data", "notepad"), 0755); err != nil {
265 | log.Fatal(err)
266 | }
267 | log.Println("Data directory created/reused without errors.")
268 |
269 | // Create placeholder notepad files if they don't exist
270 | createNotepadFileIfNotExists("md.file", mdPlaceholder)
271 | createNotepadFileIfNotExists("rtext.file", rtextPlaceholder)
272 |
273 | // Initialize the expiration tracker
274 | expirationTracker = initExpirationTracker()
275 | customExpiry := os.Getenv("DEFAULT_EXPIRY")
276 | if customExpiry != "" {
277 | if customExpiry == "1d" {
278 | expirationOptions = []string{"1 day", "Never", "1 hour", "4 hours", "Custom"}
279 | } else if customExpiry == "4h" {
280 | expirationOptions = []string{"4 hours", "Never", "1 hour", "1 day", "Custom"}
281 | } else if customExpiry == "1h" {
282 | expirationOptions = []string{"1 hour", "Never", "4 hours", "1 day", "Custom"}
283 | } else {
284 | expirationOptions = append([]string{customExpiry}, expirationOptions...)
285 | }
286 | }
287 |
288 | // Goroutine to periodically expire files
289 | go func() {
290 | ticker := time.NewTicker(3 * time.Minute) // 3 minutes is sparse enough, load is extremely minimal as the operation is fast (in memory tracker)
291 | defer ticker.Stop()
292 | for range ticker.C {
293 | expirationTracker.CleanupExpired()
294 | }
295 | }()
296 |
297 | tmpl := template.Must(template.ParseFS(content, "templates/*.html"))
298 |
299 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
300 | // Clean up expired files on page load
301 | expirationTracker.CleanupExpired()
302 |
303 | entries := []Entry{}
304 | textFiles, _ := os.ReadDir(filepath.Join("data", "text"))
305 | for _, file := range textFiles {
306 | if file.IsDir() {
307 | continue
308 | }
309 | data, err := os.ReadFile(filepath.Join("data", "text", file.Name()))
310 | if err != nil {
311 | continue
312 | }
313 | entries = append(entries, Entry{
314 | ID: filepath.Join("text", file.Name()),
315 | Type: "text",
316 | Content: string(data),
317 | Filename: file.Name(),
318 | })
319 | }
320 | files, _ := os.ReadDir(filepath.Join("data", "files"))
321 | for _, file := range files {
322 | if file.IsDir() {
323 | continue
324 | }
325 | entries = append(entries, Entry{
326 | ID: filepath.Join("files", file.Name()),
327 | Type: "file",
328 | Filename: file.Name(),
329 | })
330 | }
331 | tmpl.ExecuteTemplate(w, "index.html", entries)
332 | })
333 |
334 | http.HandleFunc("/md", func(w http.ResponseWriter, r *http.Request) {
335 | tmpl.ExecuteTemplate(w, "md.html", nil)
336 | })
337 |
338 | http.HandleFunc("/rtext", func(w http.ResponseWriter, r *http.Request) {
339 | tmpl.ExecuteTemplate(w, "rtext.html", nil)
340 | })
341 |
342 | // Retrieve custom expiration options
343 | http.HandleFunc("/getExpiryOptions", func(w http.ResponseWriter, r *http.Request) {
344 | w.Header().Set("Content-Type", "application/json")
345 | json.NewEncoder(w).Encode(expirationOptions)
346 | })
347 |
348 | // Serve static files from embedded filesystem
349 | staticFS, err := fs.Sub(content, "static")
350 | if err != nil {
351 | log.Fatalf("Failed to create static sub-filesystem: %v", err)
352 | }
353 | http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(staticFS))))
354 |
355 | http.HandleFunc("/style.css", func(w http.ResponseWriter, r *http.Request) {
356 | file, err := staticFS.Open("style.css")
357 | if err != nil {
358 | http.Error(w, "Style not found", http.StatusNotFound)
359 | return
360 | }
361 | defer file.Close()
362 | w.Header().Set("Content-Type", "text/css")
363 | io.Copy(w, file)
364 | })
365 |
366 | http.HandleFunc("/manifest.json", func(w http.ResponseWriter, r *http.Request) {
367 | file, err := staticFS.Open("manifest.json")
368 | if err != nil {
369 | http.Error(w, "Manifest not found", http.StatusNotFound)
370 | return
371 | }
372 | defer file.Close()
373 | w.Header().Set("Content-Type", "application/json")
374 | io.Copy(w, file)
375 | })
376 |
377 | http.HandleFunc("/sw.js", func(w http.ResponseWriter, r *http.Request) {
378 | file, err := staticFS.Open("sw.js")
379 | if err != nil {
380 | http.Error(w, "Service worker not found", http.StatusNotFound)
381 | return
382 | }
383 | defer file.Close()
384 | w.Header().Set("Content-Type", "application/javascript")
385 | io.Copy(w, file)
386 | })
387 |
388 | http.HandleFunc("/md.js", func(w http.ResponseWriter, r *http.Request) {
389 | file, err := staticFS.Open("md.js")
390 | if err != nil {
391 | http.Error(w, "JavaScript not found", http.StatusNotFound)
392 | return
393 | }
394 | defer file.Close()
395 | w.Header().Set("Content-Type", "application/javascript")
396 | io.Copy(w, file)
397 | })
398 |
399 | http.HandleFunc("/rtext.js", func(w http.ResponseWriter, r *http.Request) {
400 | file, err := staticFS.Open("rtext.js")
401 | if err != nil {
402 | http.Error(w, "JavaScript not found", http.StatusNotFound)
403 | return
404 | }
405 | defer file.Close()
406 | w.Header().Set("Content-Type", "application/javascript")
407 | io.Copy(w, file)
408 | })
409 |
410 | // Handle favicon and icons
411 | http.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
412 | file, err := staticFS.Open("favicon.ico")
413 | if err != nil {
414 | http.Error(w, "Favicon not found", http.StatusNotFound)
415 | return
416 | }
417 | defer file.Close()
418 | w.Header().Set("Content-Type", "image/x-icon")
419 | io.Copy(w, file)
420 | })
421 |
422 | http.HandleFunc("/icon-192.png", func(w http.ResponseWriter, r *http.Request) {
423 | file, err := staticFS.Open("icon-192.png")
424 | if err != nil {
425 | http.Error(w, "Icon not found", http.StatusNotFound)
426 | return
427 | }
428 | defer file.Close()
429 | w.Header().Set("Content-Type", "image/png")
430 | io.Copy(w, file)
431 | })
432 |
433 | http.HandleFunc("/icon-512.png", func(w http.ResponseWriter, r *http.Request) {
434 | file, err := staticFS.Open("icon-512.png")
435 | if err != nil {
436 | http.Error(w, "Icon not found", http.StatusNotFound)
437 | return
438 | }
439 | defer file.Close()
440 | w.Header().Set("Content-Type", "image/png")
441 | io.Copy(w, file)
442 | })
443 |
444 | // API endpoint to load notepad content
445 | http.HandleFunc("/notepad/", func(w http.ResponseWriter, r *http.Request) {
446 | if r.Method == "GET" {
447 | filename := strings.TrimPrefix(r.URL.Path, "/notepad/")
448 | if filename != "md.file" && filename != "rtext.file" {
449 | http.Error(w, "Invalid notepad file", http.StatusBadRequest)
450 | return
451 | }
452 | content, err := os.ReadFile(filepath.Join("data", "notepad", filename))
453 | if err != nil {
454 | http.Error(w, "Error reading notepad file", http.StatusInternalServerError)
455 | return
456 | }
457 | w.Header().Set("Content-Type", "text/plain; charset=utf-8")
458 | w.Header().Set("Cache-Control", "no-store")
459 | w.Write(content)
460 | return
461 | } else if r.Method == "POST" {
462 | filename := strings.TrimPrefix(r.URL.Path, "/notepad/")
463 | if filename != "md.file" && filename != "rtext.file" {
464 | http.Error(w, "Invalid notepad file", http.StatusBadRequest)
465 | return
466 | }
467 | content, err := io.ReadAll(r.Body)
468 | if err != nil {
469 | http.Error(w, "Error reading request body", http.StatusInternalServerError)
470 | return
471 | }
472 | err = os.WriteFile(filepath.Join("data", "notepad", filename), content, 0644)
473 | if err != nil {
474 | http.Error(w, "Error saving notepad file", http.StatusInternalServerError)
475 | return
476 | }
477 | w.WriteHeader(http.StatusOK)
478 | w.Write([]byte("Saved"))
479 | log.Printf("Saved notepad content to %s\n", filename)
480 | return
481 | }
482 | http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
483 | })
484 |
485 | http.HandleFunc("/submit", func(w http.ResponseWriter, r *http.Request) {
486 | if err := r.ParseMultipartForm(2 << 28); err != nil { // 256 MB
487 | http.Error(w, err.Error(), 500)
488 | return
489 | }
490 | entryType := r.FormValue("type")
491 | expiryOption := r.FormValue("expiry")
492 | if expiryOption == "" {
493 | expiryOption = "Never" // Default to no expiration
494 | }
495 |
496 | switch entryType {
497 | case "text":
498 | content := r.FormValue("content")
499 | if content == "" {
500 | http.Redirect(w, r, "/", http.StatusSeeOther)
501 | return
502 | }
503 | filename := r.FormValue("filename")
504 | if filename == "" {
505 | filename = time.Now().Format("Jan-02 15-04-05")
506 | } else {
507 | filename = generateUniqueFilename("data/text", filename)
508 | }
509 | err := os.WriteFile(filepath.Join("data/text", filename), []byte(content), 0644)
510 | if err != nil {
511 | http.Error(w, err.Error(), 500)
512 | return
513 | }
514 | // Set expiration if needed
515 | if expiryOption != "Never" {
516 | fileID := filepath.Join("text", filename)
517 | expirationTracker.SetExpiration(fileID, expiryOption)
518 | }
519 | log.Printf("Saved text snippet to %s with expiry %s\n", filename, expiryOption)
520 | case "file":
521 | if err := r.ParseMultipartForm(2 << 28); err != nil {
522 | http.Error(w, err.Error(), 500)
523 | log.Println("Failed to parse multipart form")
524 | return
525 | }
526 | files := r.MultipartForm.File["multifile"]
527 | if len(files) == 0 {
528 | http.Error(w, "No files uploaded", 400)
529 | log.Println("No files uploaded")
530 | return
531 | }
532 | for _, fileHeader := range files {
533 | if err := func() error {
534 | file, err := fileHeader.Open()
535 | if err != nil {
536 | return fmt.Errorf("failed to open uploaded file: %v", err)
537 | }
538 | defer file.Close()
539 | fileName := generateUniqueFilename("data/files", fileHeader.Filename)
540 | f, err := os.Create(filepath.Join("data/files", fileName))
541 | if err != nil {
542 | return fmt.Errorf("failed to create file: %v", err)
543 | }
544 | defer f.Close()
545 | if _, err := io.Copy(f, file); err != nil {
546 | return fmt.Errorf("failed to save file: %v", err)
547 | }
548 | // Set expiration if needed
549 | if expiryOption != "Never" {
550 | fileID := filepath.Join("files", fileName)
551 | expirationTracker.SetExpiration(fileID, expiryOption)
552 | }
553 | log.Printf("Saved file %s with expiry %s\n", fileName, expiryOption)
554 | return nil
555 | }(); err != nil {
556 | http.Error(w, err.Error(), 500)
557 | return
558 | }
559 | }
560 | }
561 | notifyContentChange()
562 | http.Redirect(w, r, "/", http.StatusSeeOther)
563 | })
564 |
565 | http.HandleFunc("/rename/", func(w http.ResponseWriter, r *http.Request) {
566 | if r.Method != "POST" {
567 | http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
568 | return
569 | }
570 | oldPath := strings.TrimPrefix(r.URL.Path, "/rename/")
571 | newName := r.FormValue("newname")
572 | if newName == "" {
573 | http.Error(w, "New name cannot be empty", http.StatusBadRequest)
574 | return
575 | }
576 | baseDir := filepath.Dir(filepath.Join("data", oldPath))
577 | newName = generateUniqueFilename(baseDir, newName)
578 |
579 | // Get the new full path
580 | newPath := filepath.Join(baseDir, newName)
581 | oldFullPath := filepath.Join("data", oldPath)
582 |
583 | // Check if there's an expiration for this file
584 | expirationTracker.mu.Lock()
585 | expiryTime, hasExpiry := expirationTracker.Expirations[oldPath]
586 | if hasExpiry {
587 | // Remove old entry and add new one
588 | delete(expirationTracker.Expirations, oldPath)
589 | relNewPath := strings.TrimPrefix(newPath, "data/")
590 | relNewPath = strings.TrimPrefix(relNewPath, "/")
591 | expirationTracker.Expirations[relNewPath] = expiryTime
592 | expirationTracker.saveToFile()
593 | }
594 | expirationTracker.mu.Unlock()
595 |
596 | // Rename the file
597 | err := os.Rename(oldFullPath, newPath)
598 | if err != nil {
599 | http.Error(w, err.Error(), http.StatusInternalServerError)
600 | return
601 | }
602 | notifyContentChange()
603 | http.Redirect(w, r, "/", http.StatusSeeOther)
604 | log.Printf("Renamed %s to %s\n", oldPath, newName)
605 | })
606 |
607 | http.HandleFunc("/raw/", func(w http.ResponseWriter, r *http.Request) {
608 | id := strings.TrimPrefix(r.URL.Path, "/raw/")
609 | if !strings.HasPrefix(id, "text/") {
610 | http.Error(w, "Only text files can be accessed", http.StatusBadRequest)
611 | return
612 | }
613 | content, err := os.ReadFile(filepath.Join("data", id))
614 | if err != nil {
615 | http.Error(w, "File not found", 404)
616 | return
617 | }
618 | w.Header().Set("Content-Type", "text/plain; charset=utf-8")
619 | w.Header().Set("Cache-Control", "no-store")
620 | w.Write(content)
621 | })
622 |
623 | http.HandleFunc("/show/", func(w http.ResponseWriter, r *http.Request) {
624 | id := strings.TrimPrefix(r.URL.Path, "/show/")
625 | if !strings.HasPrefix(id, "text/") {
626 | http.Error(w, "Only text files can be shown", http.StatusBadRequest)
627 | return
628 | }
629 | content, err := os.ReadFile(filepath.Join("data", id))
630 | if err != nil {
631 | http.Error(w, "File not found", 404)
632 | return
633 | }
634 | viewData := struct {
635 | Content string
636 | Filename string
637 | }{
638 | Content: string(content),
639 | Filename: filepath.Base(id),
640 | }
641 | w.Header().Set("Content-Type", "text/html; charset=utf-8")
642 | err = tmpl.ExecuteTemplate(w, "show.html", viewData)
643 | if err != nil {
644 | http.Error(w, err.Error(), http.StatusInternalServerError)
645 | }
646 | log.Printf("Served %s for viewing\n", id)
647 | })
648 |
649 | http.HandleFunc("/download/", func(w http.ResponseWriter, r *http.Request) {
650 | filename := strings.TrimPrefix(r.URL.Path, "/download/")
651 | filePath := filepath.Join("data", filename)
652 | fileInfo, err := os.Stat(filePath)
653 | if err != nil {
654 | http.Error(w, "File not found", http.StatusNotFound)
655 | return
656 | }
657 | file, err := os.Open(filePath)
658 | if err != nil {
659 | http.Error(w, err.Error(), http.StatusInternalServerError)
660 | return
661 | }
662 | defer file.Close()
663 |
664 | // Brute force method to determine content type (in practice seems better than content-disposition)
665 | ext := strings.ToLower(filepath.Ext(filename))
666 | var contentType string
667 | switch ext {
668 | case ".pdf":
669 | contentType = "application/pdf"
670 | case ".jpg", ".jpeg":
671 | contentType = "image/jpeg"
672 | case ".png":
673 | contentType = "image/png"
674 | case ".gif":
675 | contentType = "image/gif"
676 | case ".svg":
677 | contentType = "image/svg+xml"
678 | case ".mp3":
679 | contentType = "audio/mpeg"
680 | case ".mp4":
681 | contentType = "video/mp4"
682 | case ".txt":
683 | contentType = "text/plain"
684 | case ".html", ".htm":
685 | contentType = "text/html"
686 | case ".css":
687 | contentType = "text/css"
688 | case ".js":
689 | contentType = "application/javascript"
690 | case ".json":
691 | contentType = "application/json"
692 | case ".xml":
693 | contentType = "application/xml"
694 | case ".zip":
695 | contentType = "application/zip"
696 | case ".doc", ".docx":
697 | contentType = "application/msword"
698 | case ".xls", ".xlsx":
699 | contentType = "application/vnd.ms-excel"
700 | case ".ppt", ".pptx":
701 | contentType = "application/vnd.ms-powerpoint"
702 | default:
703 | // If not brute forced, detect from first 512 bytes
704 | buffer := make([]byte, 512)
705 | _, err = file.Read(buffer)
706 | if err != nil && err != io.EOF {
707 | http.Error(w, err.Error(), http.StatusInternalServerError)
708 | return
709 | }
710 | contentType = http.DetectContentType(buffer)
711 | _, err = file.Seek(0, 0)
712 | if err != nil {
713 | http.Error(w, err.Error(), http.StatusInternalServerError)
714 | return
715 | }
716 | }
717 | baseFilename := filepath.Base(filename)
718 |
719 | w.Header().Set("Content-Type", contentType)
720 | w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", baseFilename))
721 | w.Header().Set("Content-Length", fmt.Sprintf("%d", fileInfo.Size()))
722 | w.Header().Set("X-Content-Type-Options", "nosniff") // Prevent MIME sniffing: adding as best practice
723 | _, err = io.Copy(w, file)
724 | if err != nil {
725 | http.Error(w, err.Error(), http.StatusInternalServerError)
726 | return
727 | }
728 | log.Printf("Served %s for download\n", filename)
729 | })
730 |
731 | http.HandleFunc("/view/", func(w http.ResponseWriter, r *http.Request) {
732 | filename := strings.TrimPrefix(r.URL.Path, "/view/")
733 | http.ServeFile(w, r, filepath.Join("data", filename))
734 | log.Printf("Served %s for viewing\n", filename)
735 | })
736 |
737 | http.HandleFunc("/delete/", func(w http.ResponseWriter, r *http.Request) {
738 | filename := strings.TrimPrefix(r.URL.Path, "/delete/")
739 | os.Remove(filepath.Join("data", filename))
740 | expirationTracker.mu.Lock()
741 | delete(expirationTracker.Expirations, filename)
742 | expirationTracker.saveToFile()
743 | expirationTracker.mu.Unlock()
744 | notifyContentChange()
745 | http.Redirect(w, r, "/", http.StatusSeeOther)
746 | log.Printf("Deleted %s\n", filename)
747 | })
748 |
749 | http.HandleFunc("/edit/", func(w http.ResponseWriter, r *http.Request) {
750 | if r.Method != "POST" {
751 | http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
752 | return
753 | }
754 | id := strings.TrimPrefix(r.URL.Path, "/edit/")
755 | if !strings.HasPrefix(id, "text/") {
756 | http.Error(w, "Can only edit text snippets", http.StatusBadRequest)
757 | return
758 | }
759 | content := r.FormValue("content")
760 | if content == "" {
761 | http.Error(w, "Content cannot be empty", http.StatusBadRequest)
762 | return
763 | }
764 | err := os.WriteFile(filepath.Join("data", id), []byte(content), 0644)
765 | if err != nil {
766 | http.Error(w, err.Error(), http.StatusInternalServerError)
767 | return
768 | }
769 | notifyContentChange()
770 | http.Redirect(w, r, "/", http.StatusSeeOther)
771 | log.Printf("Edited %s\n", id)
772 | })
773 |
774 | // SSE Updates for content refresh
775 | http.HandleFunc("/api/updates", handleContentUpdates)
776 |
777 | // Start server
778 | log.Fatal(http.ListenAndServe(*listenAddress, nil))
779 | }
780 |
781 | // Helper function to create notepad files if they don't exist
782 | func createNotepadFileIfNotExists(filename string, defaultContent string) {
783 | filePath := filepath.Join("data", "notepad", filename)
784 | if _, err := os.Stat(filePath); os.IsNotExist(err) {
785 | err := os.WriteFile(filePath, []byte(defaultContent), 0644)
786 | if err != nil {
787 | log.Printf("Error creating notepad file %s: %v\n", filename, err)
788 | } else {
789 | log.Printf("Created notepad file %s with default content\n", filename)
790 | }
791 | }
792 | }
793 |
--------------------------------------------------------------------------------
/scripts/asset-download.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Create necessary directories
4 | mkdir -p static/fontawesome/webfonts
5 | mkdir -p static/fontawesome/css
6 | mkdir -p static/fonts
7 | mkdir -p static/js
8 | mkdir -p static/css
9 |
10 | # Download Font Awesome 6.7.2
11 | echo "Downloading Font Awesome..."
12 | curl -sL https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css -o static/fontawesome/css/all.min.css
13 | curl -sL https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/webfonts/fa-brands-400.woff2 -o static/fontawesome/webfonts/fa-brands-400.woff2
14 | curl -sL https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/webfonts/fa-regular-400.woff2 -o static/fontawesome/webfonts/fa-regular-400.woff2
15 | curl -sL https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/webfonts/fa-solid-900.woff2 -o static/fontawesome/webfonts/fa-solid-900.woff2
16 | curl -sL https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/webfonts/fa-v4compatibility.woff2 -o static/fontawesome/webfonts/fa-v4compatibility.woff2
17 |
18 | # Update CSS to use local webfonts
19 | sed -i.bak 's|https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/webfonts/|/static/fontawesome/webfonts/|g' static/fontawesome/css/all.min.css
20 | rm static/fontawesome/css/all.min.css.bak
21 |
22 | # Download Inter font from Google Fonts
23 | echo "Downloading Inter font..."
24 | curl -sL "https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" -o static/css/inter.css
25 |
26 | # Download font files referenced in the CSS
27 | grep -o "https://fonts.gstatic.com/s/inter/[^)]*" static/css/inter.css | while read -r url; do
28 | filename=$(basename "$url")
29 | curl -sL "$url" -o "static/fonts/$filename"
30 | done
31 |
32 | # Update font CSS to use local files
33 | sed -i.bak 's|https://fonts.gstatic.com/s/inter/v../|/static/fonts/|g' static/css/inter.css
34 | rm static/css/inter.css.bak
35 |
36 | # Download Highlight.js
37 | echo "Downloading Highlight.js..."
38 | curl -sL https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js -o static/js/highlight.min.js
39 |
40 | # Download Catppuccin theme for Highlight.js
41 | echo "Downloading Catppuccin theme for Highlight.js..."
42 | curl -sL https://cdn.jsdelivr.net/npm/@catppuccin/highlightjs@0.1.1/css/catppuccin-latte.css -o static/css/catppuccin-latte.css
43 | curl -sL https://cdn.jsdelivr.net/npm/@catppuccin/highlightjs@0.1.1/css/catppuccin-mocha.css -o static/css/catppuccin-mocha.css
44 |
45 | # Download Marked.js for markdown parsing
46 | echo "Downloading Marked.js..."
47 | curl -sL https://cdnjs.cloudflare.com/ajax/libs/marked/4.3.0/marked.min.js -o static/js/marked.min.js
48 |
49 | echo "Assets downloaded and configured successfully!"
50 |
--------------------------------------------------------------------------------
/static/css/catppuccin-latte.css:
--------------------------------------------------------------------------------
1 | code.hljs{color:#4c4f69;background:#eff1f5}code .hljs-keyword{color:#8839ef}code .hljs-built_in{color:#d20f39}code .hljs-type{color:#df8e1d}code .hljs-literal{color:#fe640b}code .hljs-number{color:#fe640b}code .hljs-operator{color:#179299}code .hljs-punctuation{color:#5c5f77}code .hljs-property{color:#179299}code .hljs-regexp{color:#ea76cb}code .hljs-string{color:#40a02b}code .hljs-char.escape_{color:#40a02b}code .hljs-subst{color:#6c6f85}code .hljs-symbol{color:#dd7878}code .hljs-variable{color:#8839ef}code .hljs-variable.language_{color:#8839ef}code .hljs-variable.constant_{color:#fe640b}code .hljs-title{color:#1e66f5}code .hljs-title.class_{color:#df8e1d}code .hljs-title.function_{color:#1e66f5}code .hljs-params{color:#4c4f69}code .hljs-comment{color:#acb0be}code .hljs-doctag{color:#d20f39}code .hljs-meta{color:#fe640b}code .hljs-section{color:#1e66f5}code .hljs-tag{color:#6c6f85}code .hljs-name{color:#8839ef}code .hljs-attr{color:#1e66f5}code .hljs-attribute{color:#40a02b}code .hljs-bullet{color:#179299}code .hljs-code{color:#40a02b}code .hljs-emphasis{color:#d20f39;font-style:italic}code .hljs-strong{color:#d20f39;font-weight:bold}code .hljs-formula{color:#179299}code .hljs-link{color:#209fb5;font-style:italic}code .hljs-quote{color:#40a02b;font-style:italic}code .hljs-selector-tag{color:#df8e1d}code .hljs-selector-id{color:#1e66f5}code .hljs-selector-class{color:#179299}code .hljs-selector-attr{color:#8839ef}code .hljs-selector-pseudo{color:#179299}code .hljs-template-tag{color:#dd7878}code .hljs-template-variable{color:#dd7878}code .hljs-diff-addition{color:#40a02b;background:rgba(var(--ctp-green), 15%)}code .hljs-diff-deletion{color:#d20f39;background:rgba(var(--ctp-red), 15%)}
2 |
--------------------------------------------------------------------------------
/static/css/catppuccin-mocha.css:
--------------------------------------------------------------------------------
1 | code.hljs{color:#cdd6f4;background:#1e1e2e}code .hljs-keyword{color:#cba6f7}code .hljs-built_in{color:#f38ba8}code .hljs-type{color:#f9e2af}code .hljs-literal{color:#fab387}code .hljs-number{color:#fab387}code .hljs-operator{color:#94e2d5}code .hljs-punctuation{color:#bac2de}code .hljs-property{color:#94e2d5}code .hljs-regexp{color:#f5c2e7}code .hljs-string{color:#a6e3a1}code .hljs-char.escape_{color:#a6e3a1}code .hljs-subst{color:#a6adc8}code .hljs-symbol{color:#f2cdcd}code .hljs-variable{color:#cba6f7}code .hljs-variable.language_{color:#cba6f7}code .hljs-variable.constant_{color:#fab387}code .hljs-title{color:#89b4fa}code .hljs-title.class_{color:#f9e2af}code .hljs-title.function_{color:#89b4fa}code .hljs-params{color:#cdd6f4}code .hljs-comment{color:#585b70}code .hljs-doctag{color:#f38ba8}code .hljs-meta{color:#fab387}code .hljs-section{color:#89b4fa}code .hljs-tag{color:#a6adc8}code .hljs-name{color:#cba6f7}code .hljs-attr{color:#89b4fa}code .hljs-attribute{color:#a6e3a1}code .hljs-bullet{color:#94e2d5}code .hljs-code{color:#a6e3a1}code .hljs-emphasis{color:#f38ba8;font-style:italic}code .hljs-strong{color:#f38ba8;font-weight:bold}code .hljs-formula{color:#94e2d5}code .hljs-link{color:#74c7ec;font-style:italic}code .hljs-quote{color:#a6e3a1;font-style:italic}code .hljs-selector-tag{color:#f9e2af}code .hljs-selector-id{color:#89b4fa}code .hljs-selector-class{color:#94e2d5}code .hljs-selector-attr{color:#cba6f7}code .hljs-selector-pseudo{color:#94e2d5}code .hljs-template-tag{color:#f2cdcd}code .hljs-template-variable{color:#f2cdcd}code .hljs-diff-addition{color:#a6e3a1;background:rgba(var(--ctp-green), 15%)}code .hljs-diff-deletion{color:#f38ba8;background:rgba(var(--ctp-red), 15%)}
2 |
--------------------------------------------------------------------------------
/static/css/inter.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: 'Inter';
3 | font-style: normal;
4 | font-weight: 400;
5 | font-display: swap;
6 | src: url(/static/fonts/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfMZg.ttf) format('truetype');
7 | }
8 | @font-face {
9 | font-family: 'Inter';
10 | font-style: normal;
11 | font-weight: 500;
12 | font-display: swap;
13 | src: url(/static/fonts/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuI6fMZg.ttf) format('truetype');
14 | }
15 | @font-face {
16 | font-family: 'Inter';
17 | font-style: normal;
18 | font-weight: 600;
19 | font-display: swap;
20 | src: url(/static/fonts/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuGKYMZg.ttf) format('truetype');
21 | }
22 | @font-face {
23 | font-family: 'Inter';
24 | font-style: normal;
25 | font-weight: 700;
26 | font-display: swap;
27 | src: url(/static/fonts/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuFuYMZg.ttf) format('truetype');
28 | }
29 | @font-face {
30 | font-family: 'Inter';
31 | font-style: normal;
32 | font-weight: 800;
33 | font-display: swap;
34 | src: url(/static/fonts/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuDyYMZg.ttf) format('truetype');
35 | }
36 |
--------------------------------------------------------------------------------
/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tanq16/local-content-share/00ad44b8d9b6999872ad97b510343ba858a76de4/static/favicon.ico
--------------------------------------------------------------------------------
/static/fontawesome/webfonts/fa-brands-400.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tanq16/local-content-share/00ad44b8d9b6999872ad97b510343ba858a76de4/static/fontawesome/webfonts/fa-brands-400.woff2
--------------------------------------------------------------------------------
/static/fontawesome/webfonts/fa-regular-400.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tanq16/local-content-share/00ad44b8d9b6999872ad97b510343ba858a76de4/static/fontawesome/webfonts/fa-regular-400.woff2
--------------------------------------------------------------------------------
/static/fontawesome/webfonts/fa-solid-900.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tanq16/local-content-share/00ad44b8d9b6999872ad97b510343ba858a76de4/static/fontawesome/webfonts/fa-solid-900.woff2
--------------------------------------------------------------------------------
/static/fontawesome/webfonts/fa-v4compatibility.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tanq16/local-content-share/00ad44b8d9b6999872ad97b510343ba858a76de4/static/fontawesome/webfonts/fa-v4compatibility.woff2
--------------------------------------------------------------------------------
/static/fonts/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuDyYMZg.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tanq16/local-content-share/00ad44b8d9b6999872ad97b510343ba858a76de4/static/fonts/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuDyYMZg.ttf
--------------------------------------------------------------------------------
/static/fonts/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuFuYMZg.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tanq16/local-content-share/00ad44b8d9b6999872ad97b510343ba858a76de4/static/fonts/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuFuYMZg.ttf
--------------------------------------------------------------------------------
/static/fonts/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuGKYMZg.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tanq16/local-content-share/00ad44b8d9b6999872ad97b510343ba858a76de4/static/fonts/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuGKYMZg.ttf
--------------------------------------------------------------------------------
/static/fonts/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuI6fMZg.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tanq16/local-content-share/00ad44b8d9b6999872ad97b510343ba858a76de4/static/fonts/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuI6fMZg.ttf
--------------------------------------------------------------------------------
/static/fonts/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfMZg.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tanq16/local-content-share/00ad44b8d9b6999872ad97b510343ba858a76de4/static/fonts/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfMZg.ttf
--------------------------------------------------------------------------------
/static/icon-192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tanq16/local-content-share/00ad44b8d9b6999872ad97b510343ba858a76de4/static/icon-192.png
--------------------------------------------------------------------------------
/static/icon-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tanq16/local-content-share/00ad44b8d9b6999872ad97b510343ba858a76de4/static/icon-512.png
--------------------------------------------------------------------------------
/static/js/marked.min.js:
--------------------------------------------------------------------------------
1 | /**
2 | * marked v4.3.0 - a markdown parser
3 | * Copyright (c) 2011-2023, Christopher Jeffrey. (MIT Licensed)
4 | * https://github.com/markedjs/marked
5 | */
6 | !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).marked={})}(this,function(r){"use strict";function i(e,t){for(var u=0;ue.length)&&(t=e.length);for(var u=0,n=new Array(t);u=e.length?{done:!0}:{done:!1,value:e[u++]}};throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}function e(){return{async:!1,baseUrl:null,breaks:!1,extensions:null,gfm:!0,headerIds:!0,headerPrefix:"",highlight:null,hooks:null,langPrefix:"language-",mangle:!0,pedantic:!1,renderer:null,sanitize:!1,sanitizer:null,silent:!1,smartypants:!1,tokenizer:null,walkTokens:null,xhtml:!1}}r.defaults=e();function u(e){return t[e]}var n=/[&<>"']/,l=new RegExp(n.source,"g"),o=/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/,a=new RegExp(o.source,"g"),t={"&":"&","<":"<",">":">",'"':""","'":"'"};function A(e,t){if(t){if(n.test(e))return e.replace(l,u)}else if(o.test(e))return e.replace(a,u);return e}var c=/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/gi;function x(e){return e.replace(c,function(e,t){return"colon"===(t=t.toLowerCase())?":":"#"===t.charAt(0)?"x"===t.charAt(1)?String.fromCharCode(parseInt(t.substring(2),16)):String.fromCharCode(+t.substring(1)):""})}var h=/(^|[^\[])\^/g;function p(u,e){u="string"==typeof u?u:u.source,e=e||"";var n={replace:function(e,t){return t=(t=t.source||t).replace(h,"$1"),u=u.replace(e,t),n},getRegex:function(){return new RegExp(u,e)}};return n}var Z=/[^\w:]/g,O=/^$|^[a-z][a-z0-9+.-]*:|^[?#]/i;function f(e,t,u){if(e){try{n=decodeURIComponent(x(u)).replace(Z,"").toLowerCase()}catch(e){return null}if(0===n.indexOf("javascript:")||0===n.indexOf("vbscript:")||0===n.indexOf("data:"))return null}var n;t&&!O.test(u)&&(e=u,g[" "+(n=t)]||(q.test(n)?g[" "+n]=n+"/":g[" "+n]=C(n,"/",!0)),t=-1===(n=g[" "+n]).indexOf(":"),u="//"===e.substring(0,2)?t?e:n.replace(j,"$1")+e:"/"===e.charAt(0)?t?e:n.replace(P,"$1")+e:n+e);try{u=encodeURI(u).replace(/%25/g,"%")}catch(e){return null}return u}var g={},q=/^[^:]+:\/*[^/]*$/,j=/^([^:]+:)[\s\S]*$/,P=/^([^:]+:\/*[^/]*)[\s\S]*$/;var k={exec:function(){}};function d(e,t){var u=e.replace(/\|/g,function(e,t,u){for(var n=!1,r=t;0<=--r&&"\\"===u[r];)n=!n;return n?"|":" |"}).split(/ \|/),n=0;if(u[0].trim()||u.shift(),0t)u.splice(t);else for(;u.length>=1,e+=e;return u+e}function m(e,t,u,n){var r=t.href,t=t.title?A(t.title):null,i=e[1].replace(/\\([\[\]])/g,"$1");return"!"!==e[0].charAt(0)?(n.state.inLink=!0,e={type:"link",raw:u,href:r,title:t,text:i,tokens:n.inlineTokens(i)},n.state.inLink=!1,e):{type:"image",raw:u,href:r,title:t,text:A(i)}}var b=function(){function e(e){this.options=e||r.defaults}var t=e.prototype;return t.space=function(e){e=this.rules.block.newline.exec(e);if(e&&0=r.length?e.slice(r.length):e}).join("\n")),{type:"code",raw:t,lang:e[2]&&e[2].trim().replace(this.rules.inline._escapes,"$1"),text:u}},t.heading=function(e){var t,u,e=this.rules.block.heading.exec(e);if(e)return t=e[2].trim(),/#$/.test(t)&&(u=C(t,"#"),!this.options.pedantic&&u&&!/ $/.test(u)||(t=u.trim())),{type:"heading",raw:e[0],depth:e[1].length,text:t,tokens:this.lexer.inline(t)}},t.hr=function(e){e=this.rules.block.hr.exec(e);if(e)return{type:"hr",raw:e[0]}},t.blockquote=function(e){var t,u,n,e=this.rules.block.blockquote.exec(e);if(e)return t=e[0].replace(/^ *>[ \t]?/gm,""),u=this.lexer.state.top,this.lexer.state.top=!0,n=this.lexer.blockTokens(t),this.lexer.state.top=u,{type:"blockquote",raw:e[0],tokens:n,text:t}},t.list=function(e){var t=this.rules.block.list.exec(e);if(t){var u,n,r,i,s,l,o,a,D,c,h,p=1<(g=t[1].trim()).length,f={type:"list",raw:"",ordered:p,start:p?+g.slice(0,-1):"",loose:!1,items:[]},g=p?"\\d{1,9}\\"+g.slice(-1):"\\"+g;this.options.pedantic&&(g=p?g:"[*+-]");for(var F=new RegExp("^( {0,3}"+g+")((?:[\t ][^\\n]*)?(?:\\n|$))");e&&(h=!1,t=F.exec(e))&&!this.rules.block.hr.test(e);){if(u=t[0],e=e.substring(u.length),o=t[2].split("\n",1)[0].replace(/^\t+/,function(e){return" ".repeat(3*e.length)}),a=e.split("\n",1)[0],this.options.pedantic?(i=2,c=o.trimLeft()):(i=t[2].search(/[^ ]/),c=o.slice(i=4=i||!a.trim())c+="\n"+a.slice(i);else{if(s)break;if(4<=o.search(/[^ ]/))break;if(d.test(o))break;if(C.test(o))break;if(k.test(o))break;c+="\n"+a}s||a.trim()||(s=!0),u+=D+"\n",e=e.substring(D.length+1),o=a.slice(i)}f.loose||(l?f.loose=!0:/\n *\n *$/.test(u)&&(l=!0)),this.options.gfm&&(n=/^\[[ xX]\] /.exec(c))&&(r="[ ] "!==n[0],c=c.replace(/^\[[ xX]\] +/,"")),f.items.push({type:"list_item",raw:u,task:!!n,checked:r,loose:!1,text:c}),f.raw+=u}f.items[f.items.length-1].raw=u.trimRight(),f.items[f.items.length-1].text=c.trimRight(),f.raw=f.raw.trimRight();for(var E,x=f.items.length,m=0;m$/,"$1").replace(this.rules.inline._escapes,"$1"):"",n=e[3]&&e[3].substring(1,e[3].length-1).replace(this.rules.inline._escapes,"$1"),{type:"def",tag:t,raw:e[0],href:u,title:n}},t.table=function(e){e=this.rules.block.table.exec(e);if(e){var t={type:"table",header:d(e[1]).map(function(e){return{text:e}}),align:e[2].replace(/^ *|\| *$/g,"").split(/ *\| */),rows:e[3]&&e[3].trim()?e[3].replace(/\n[ \t]*$/,"").split("\n"):[]};if(t.header.length===t.align.length){t.raw=e[0];for(var u,n,r,i=t.align.length,s=0;s/i.test(e[0])&&(this.lexer.state.inLink=!1),!this.lexer.state.inRawBlock&&/^<(pre|code|kbd|script)(\s|>)/i.test(e[0])?this.lexer.state.inRawBlock=!0:this.lexer.state.inRawBlock&&/^<\/(pre|code|kbd|script)(\s|>)/i.test(e[0])&&(this.lexer.state.inRawBlock=!1),{type:this.options.sanitize?"text":"html",raw:e[0],inLink:this.lexer.state.inLink,inRawBlock:this.lexer.state.inRawBlock,text:this.options.sanitize?this.options.sanitizer?this.options.sanitizer(e[0]):A(e[0]):e[0]}},t.link=function(e){e=this.rules.inline.link.exec(e);if(e){var t=e[2].trim();if(!this.options.pedantic&&/^$/.test(t))return;var u=C(t.slice(0,-1),"\\");if((t.length-u.length)%2==0)return}else{u=function(e,t){if(-1!==e.indexOf(t[1]))for(var u=e.length,n=0,r=0;r $/.test(t)?u.slice(1):u.slice(1,-1):u)&&u.replace(this.rules.inline._escapes,"$1"),title:r&&r.replace(this.rules.inline._escapes,"$1")},e[0],this.lexer)}},t.reflink=function(e,t){var u;if(u=(u=this.rules.inline.reflink.exec(e))||this.rules.inline.nolink.exec(e))return(e=t[(e=(u[2]||u[1]).replace(/\s+/g," ")).toLowerCase()])?m(u,e,u[0],this.lexer):{type:"text",raw:t=u[0].charAt(0),text:t}},t.emStrong=function(e,t,u){void 0===u&&(u="");var n=this.rules.inline.emStrong.lDelim.exec(e);if(n&&(!n[3]||!u.match(/(?:[0-9A-Za-z\xAA\xB2\xB3\xB5\xB9\xBA\xBC-\xBE\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0560-\u0588\u05D0-\u05EA\u05EF-\u05F2\u0620-\u064A\u0660-\u0669\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07C0-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u0860-\u086A\u0870-\u0887\u0889-\u088E\u08A0-\u08C9\u0904-\u0939\u093D\u0950\u0958-\u0961\u0966-\u096F\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09E6-\u09F1\u09F4-\u09F9\u09FC\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A66-\u0A6F\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0AE6-\u0AEF\u0AF9\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B66-\u0B6F\u0B71-\u0B77\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0BE6-\u0BF2\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C5D\u0C60\u0C61\u0C66-\u0C6F\u0C78-\u0C7E\u0C80\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDD\u0CDE\u0CE0\u0CE1\u0CE6-\u0CEF\u0CF1\u0CF2\u0D04-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D54-\u0D56\u0D58-\u0D61\u0D66-\u0D78\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0DE6-\u0DEF\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E50-\u0E59\u0E81\u0E82\u0E84\u0E86-\u0E8A\u0E8C-\u0EA3\u0EA5\u0EA7-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0ED0-\u0ED9\u0EDC-\u0EDF\u0F00\u0F20-\u0F33\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F-\u1049\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u1090-\u1099\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1369-\u137C\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F8\u1700-\u1711\u171F-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u17E0-\u17E9\u17F0-\u17F9\u1810-\u1819\u1820-\u1878\u1880-\u1884\u1887-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1946-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u19D0-\u19DA\u1A00-\u1A16\u1A20-\u1A54\u1A80-\u1A89\u1A90-\u1A99\u1AA7\u1B05-\u1B33\u1B45-\u1B4C\u1B50-\u1B59\u1B83-\u1BA0\u1BAE-\u1BE5\u1C00-\u1C23\u1C40-\u1C49\u1C4D-\u1C7D\u1C80-\u1C88\u1C90-\u1CBA\u1CBD-\u1CBF\u1CE9-\u1CEC\u1CEE-\u1CF3\u1CF5\u1CF6\u1CFA\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2070\u2071\u2074-\u2079\u207F-\u2089\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2150-\u2189\u2460-\u249B\u24EA-\u24FF\u2776-\u2793\u2C00-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2CFD\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005-\u3007\u3021-\u3029\u3031-\u3035\u3038-\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312F\u3131-\u318E\u3192-\u3195\u31A0-\u31BF\u31F0-\u31FF\u3220-\u3229\u3248-\u324F\u3251-\u325F\u3280-\u3289\u32B1-\u32BF\u3400-\u4DBF\u4E00-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6EF\uA717-\uA71F\uA722-\uA788\uA78B-\uA7CA\uA7D0\uA7D1\uA7D3\uA7D5-\uA7D9\uA7F2-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA830-\uA835\uA840-\uA873\uA882-\uA8B3\uA8D0-\uA8D9\uA8F2-\uA8F7\uA8FB\uA8FD\uA8FE\uA900-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF-\uA9D9\uA9E0-\uA9E4\uA9E6-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA50-\uAA59\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB69\uAB70-\uABE2\uABF0-\uABF9\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]|\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDD07-\uDD33\uDD40-\uDD78\uDD8A\uDD8B\uDE80-\uDE9C\uDEA0-\uDED0\uDEE1-\uDEFB\uDF00-\uDF23\uDF2D-\uDF4A\uDF50-\uDF75\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF\uDFD1-\uDFD5]|\uD801[\uDC00-\uDC9D\uDCA0-\uDCA9\uDCB0-\uDCD3\uDCD8-\uDCFB\uDD00-\uDD27\uDD30-\uDD63\uDD70-\uDD7A\uDD7C-\uDD8A\uDD8C-\uDD92\uDD94\uDD95\uDD97-\uDDA1\uDDA3-\uDDB1\uDDB3-\uDDB9\uDDBB\uDDBC\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67\uDF80-\uDF85\uDF87-\uDFB0\uDFB2-\uDFBA]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC58-\uDC76\uDC79-\uDC9E\uDCA7-\uDCAF\uDCE0-\uDCF2\uDCF4\uDCF5\uDCFB-\uDD1B\uDD20-\uDD39\uDD80-\uDDB7\uDDBC-\uDDCF\uDDD2-\uDE00\uDE10-\uDE13\uDE15-\uDE17\uDE19-\uDE35\uDE40-\uDE48\uDE60-\uDE7E\uDE80-\uDE9F\uDEC0-\uDEC7\uDEC9-\uDEE4\uDEEB-\uDEEF\uDF00-\uDF35\uDF40-\uDF55\uDF58-\uDF72\uDF78-\uDF91\uDFA9-\uDFAF]|\uD803[\uDC00-\uDC48\uDC80-\uDCB2\uDCC0-\uDCF2\uDCFA-\uDD23\uDD30-\uDD39\uDE60-\uDE7E\uDE80-\uDEA9\uDEB0\uDEB1\uDF00-\uDF27\uDF30-\uDF45\uDF51-\uDF54\uDF70-\uDF81\uDFB0-\uDFCB\uDFE0-\uDFF6]|\uD804[\uDC03-\uDC37\uDC52-\uDC6F\uDC71\uDC72\uDC75\uDC83-\uDCAF\uDCD0-\uDCE8\uDCF0-\uDCF9\uDD03-\uDD26\uDD36-\uDD3F\uDD44\uDD47\uDD50-\uDD72\uDD76\uDD83-\uDDB2\uDDC1-\uDDC4\uDDD0-\uDDDA\uDDDC\uDDE1-\uDDF4\uDE00-\uDE11\uDE13-\uDE2B\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA8\uDEB0-\uDEDE\uDEF0-\uDEF9\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3D\uDF50\uDF5D-\uDF61]|\uD805[\uDC00-\uDC34\uDC47-\uDC4A\uDC50-\uDC59\uDC5F-\uDC61\uDC80-\uDCAF\uDCC4\uDCC5\uDCC7\uDCD0-\uDCD9\uDD80-\uDDAE\uDDD8-\uDDDB\uDE00-\uDE2F\uDE44\uDE50-\uDE59\uDE80-\uDEAA\uDEB8\uDEC0-\uDEC9\uDF00-\uDF1A\uDF30-\uDF3B\uDF40-\uDF46]|\uD806[\uDC00-\uDC2B\uDCA0-\uDCF2\uDCFF-\uDD06\uDD09\uDD0C-\uDD13\uDD15\uDD16\uDD18-\uDD2F\uDD3F\uDD41\uDD50-\uDD59\uDDA0-\uDDA7\uDDAA-\uDDD0\uDDE1\uDDE3\uDE00\uDE0B-\uDE32\uDE3A\uDE50\uDE5C-\uDE89\uDE9D\uDEB0-\uDEF8]|\uD807[\uDC00-\uDC08\uDC0A-\uDC2E\uDC40\uDC50-\uDC6C\uDC72-\uDC8F\uDD00-\uDD06\uDD08\uDD09\uDD0B-\uDD30\uDD46\uDD50-\uDD59\uDD60-\uDD65\uDD67\uDD68\uDD6A-\uDD89\uDD98\uDDA0-\uDDA9\uDEE0-\uDEF2\uDFB0\uDFC0-\uDFD4]|\uD808[\uDC00-\uDF99]|\uD809[\uDC00-\uDC6E\uDC80-\uDD43]|\uD80B[\uDF90-\uDFF0]|[\uD80C\uD81C-\uD820\uD822\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879\uD880-\uD883][\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2E]|\uD811[\uDC00-\uDE46]|\uD81A[\uDC00-\uDE38\uDE40-\uDE5E\uDE60-\uDE69\uDE70-\uDEBE\uDEC0-\uDEC9\uDED0-\uDEED\uDF00-\uDF2F\uDF40-\uDF43\uDF50-\uDF59\uDF5B-\uDF61\uDF63-\uDF77\uDF7D-\uDF8F]|\uD81B[\uDE40-\uDE96\uDF00-\uDF4A\uDF50\uDF93-\uDF9F\uDFE0\uDFE1\uDFE3]|\uD821[\uDC00-\uDFF7]|\uD823[\uDC00-\uDCD5\uDD00-\uDD08]|\uD82B[\uDFF0-\uDFF3\uDFF5-\uDFFB\uDFFD\uDFFE]|\uD82C[\uDC00-\uDD22\uDD50-\uDD52\uDD64-\uDD67\uDD70-\uDEFB]|\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99]|\uD834[\uDEE0-\uDEF3\uDF60-\uDF78]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB\uDFCE-\uDFFF]|\uD837[\uDF00-\uDF1E]|\uD838[\uDD00-\uDD2C\uDD37-\uDD3D\uDD40-\uDD49\uDD4E\uDE90-\uDEAD\uDEC0-\uDEEB\uDEF0-\uDEF9]|\uD839[\uDFE0-\uDFE6\uDFE8-\uDFEB\uDFED\uDFEE\uDFF0-\uDFFE]|\uD83A[\uDC00-\uDCC4\uDCC7-\uDCCF\uDD00-\uDD43\uDD4B\uDD50-\uDD59]|\uD83B[\uDC71-\uDCAB\uDCAD-\uDCAF\uDCB1-\uDCB4\uDD01-\uDD2D\uDD2F-\uDD3D\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]|\uD83C[\uDD00-\uDD0C]|\uD83E[\uDFF0-\uDFF9]|\uD869[\uDC00-\uDEDF\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF38\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0]|\uD87E[\uDC00-\uDE1D]|\uD884[\uDC00-\uDF4A])/))){var r=n[1]||n[2]||"";if(!r||""===u||this.rules.inline.punctuation.exec(u)){var i=n[0].length-1,s=i,l=0,o="*"===n[0][0]?this.rules.inline.emStrong.rDelimAst:this.rules.inline.emStrong.rDelimUnd;for(o.lastIndex=0,t=t.slice(-1*e.length+i);null!=(n=o.exec(t));){var a,D=n[1]||n[2]||n[3]||n[4]||n[5]||n[6];if(D)if(a=D.length,n[3]||n[4])s+=a;else if((n[5]||n[6])&&i%3&&!((i+a)%3))l+=a;else if(!(0<(s-=a)))return a=Math.min(a,a+s+l),D=e.slice(0,i+n.index+(n[0].length-D.length)+a),Math.min(i,a)%2?(a=D.slice(1,-1),{type:"em",raw:D,text:a,tokens:this.lexer.inlineTokens(a)}):(a=D.slice(2,-2),{type:"strong",raw:D,text:a,tokens:this.lexer.inlineTokens(a)})}}}},t.codespan=function(e){var t,u,n,e=this.rules.inline.code.exec(e);if(e)return n=e[2].replace(/\n/g," "),t=/[^ ]/.test(n),u=/^ /.test(n)&&/ $/.test(n),n=A(n=t&&u?n.substring(1,n.length-1):n,!0),{type:"codespan",raw:e[0],text:n}},t.br=function(e){e=this.rules.inline.br.exec(e);if(e)return{type:"br",raw:e[0]}},t.del=function(e){e=this.rules.inline.del.exec(e);if(e)return{type:"del",raw:e[0],text:e[2],tokens:this.lexer.inlineTokens(e[2])}},t.autolink=function(e,t){var u,e=this.rules.inline.autolink.exec(e);if(e)return t="@"===e[2]?"mailto:"+(u=A(this.options.mangle?t(e[1]):e[1])):u=A(e[1]),{type:"link",raw:e[0],text:u,href:t,tokens:[{type:"text",raw:u,text:u}]}},t.url=function(e,t){var u,n,r,i;if(u=this.rules.inline.url.exec(e)){if("@"===u[2])r="mailto:"+(n=A(this.options.mangle?t(u[0]):u[0]));else{for(;i=u[0],u[0]=this.rules.inline._backpedal.exec(u[0])[0],i!==u[0];);n=A(u[0]),r="www."===u[1]?"http://"+u[0]:u[0]}return{type:"link",raw:u[0],text:n,href:r,tokens:[{type:"text",raw:n,text:n}]}}},t.inlineText=function(e,t){e=this.rules.inline.text.exec(e);if(e)return t=this.lexer.state.inRawBlock?this.options.sanitize?this.options.sanitizer?this.options.sanitizer(e[0]):A(e[0]):e[0]:A(this.options.smartypants?t(e[0]):e[0]),{type:"text",raw:e[0],text:t}},e}(),B={newline:/^(?: *(?:\n|$))+/,code:/^( {4}[^\n]+(?:\n(?: *(?:\n|$))*)?)+/,fences:/^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/,hr:/^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/,heading:/^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,blockquote:/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/,list:/^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/,html:"^ {0,3}(?:<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:\\1>[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|\\n*|$)|\\n*|$)|?(tag)(?: +|\\n|/?>)[\\s\\S]*?(?:(?:\\n *)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$)|(?!script|pre|style|textarea)[a-z][\\w-]*\\s*>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$))",def:/^ {0,3}\[(label)\]: *(?:\n *)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n *)?| *\n *)(title))? *(?:\n+|$)/,table:k,lheading:/^((?:.|\n(?!\n))+?)\n {0,3}(=+|-+) *(?:\n+|$)/,_paragraph:/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/,text:/^[^\n]+/,_label:/(?!\s*\])(?:\\.|[^\[\]\\])+/,_title:/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/},w=(B.def=p(B.def).replace("label",B._label).replace("title",B._title).getRegex(),B.bullet=/(?:[*+-]|\d{1,9}[.)])/,B.listItemStart=p(/^( *)(bull) */).replace("bull",B.bullet).getRegex(),B.list=p(B.list).replace(/bull/g,B.bullet).replace("hr","\\n+(?=\\1?(?:(?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$))").replace("def","\\n+(?="+B.def.source+")").getRegex(),B._tag="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|section|source|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",B._comment=/|$)/,B.html=p(B.html,"i").replace("comment",B._comment).replace("tag",B._tag).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),B.paragraph=p(B._paragraph).replace("hr",B.hr).replace("heading"," {0,3}#{1,6} ").replace("|lheading","").replace("|table","").replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html","?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)").replace("tag",B._tag).getRegex(),B.blockquote=p(B.blockquote).replace("paragraph",B.paragraph).getRegex(),B.normal=F({},B),B.gfm=F({},B.normal,{table:"^ *([^\\n ].*\\|.*)\\n {0,3}(?:\\| *)?(:?-+:? *(?:\\| *:?-+:? *)*)(?:\\| *)?(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)"}),B.gfm.table=p(B.gfm.table).replace("hr",B.hr).replace("heading"," {0,3}#{1,6} ").replace("blockquote"," {0,3}>").replace("code"," {4}[^\\n]").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html","?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)").replace("tag",B._tag).getRegex(),B.gfm.paragraph=p(B._paragraph).replace("hr",B.hr).replace("heading"," {0,3}#{1,6} ").replace("|lheading","").replace("table",B.gfm.table).replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html","?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)").replace("tag",B._tag).getRegex(),B.pedantic=F({},B.normal,{html:p("^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+?\\1> *(?:\\n{2,}|\\s*$)| \\s]*)*?/?> *(?:\\n{2,}|\\s*$))").replace("comment",B._comment).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *([^\s>]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/,heading:/^(#{1,6})(.*)(?:\n+|$)/,fences:k,lheading:/^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/,paragraph:p(B.normal._paragraph).replace("hr",B.hr).replace("heading"," *#{1,6} *[^\n]").replace("lheading",B.lheading).replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").getRegex()}),{escape:/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,autolink:/^<(scheme:[^\s\x00-\x1f<>]*|email)>/,url:k,tag:"^comment|^[a-zA-Z][\\w:-]*\\s*>|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^|^",link:/^!?\[(label)\]\(\s*(href)(?:\s+(title))?\s*\)/,reflink:/^!?\[(label)\]\[(ref)\]/,nolink:/^!?\[(ref)\](?:\[\])?/,reflinkSearch:"reflink|nolink(?!\\()",emStrong:{lDelim:/^(?:\*+(?:([punct_])|[^\s*]))|^_+(?:([punct*])|([^\s_]))/,rDelimAst:/^(?:[^_*\\]|\\.)*?\_\_(?:[^_*\\]|\\.)*?\*(?:[^_*\\]|\\.)*?(?=\_\_)|(?:[^*\\]|\\.)+(?=[^*])|[punct_](\*+)(?=[\s]|$)|(?:[^punct*_\s\\]|\\.)(\*+)(?=[punct_\s]|$)|[punct_\s](\*+)(?=[^punct*_\s])|[\s](\*+)(?=[punct_])|[punct_](\*+)(?=[punct_])|(?:[^punct*_\s\\]|\\.)(\*+)(?=[^punct*_\s])/,rDelimUnd:/^(?:[^_*\\]|\\.)*?\*\*(?:[^_*\\]|\\.)*?\_(?:[^_*\\]|\\.)*?(?=\*\*)|(?:[^_\\]|\\.)+(?=[^_])|[punct*](\_+)(?=[\s]|$)|(?:[^punct*_\s\\]|\\.)(\_+)(?=[punct*\s]|$)|[punct*\s](\_+)(?=[^punct*_\s])|[\s](\_+)(?=[punct*])|[punct*](\_+)(?=[punct*])/},code:/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,br:/^( {2,}|\\)\n(?!\s*$)/,del:k,text:/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\?@\\[\\]`^{|}~",w.punctuation=p(w.punctuation).replace(/punctuation/g,w._punctuation).getRegex(),w.blockSkip=/\[[^\]]*?\]\([^\)]*?\)|`[^`]*?`|<[^>]*?>/g,w.escapedEmSt=/(?:^|[^\\])(?:\\\\)*\\[*_]/g,w._comment=p(B._comment).replace("(?:--\x3e|$)","--\x3e").getRegex(),w.emStrong.lDelim=p(w.emStrong.lDelim).replace(/punct/g,w._punctuation).getRegex(),w.emStrong.rDelimAst=p(w.emStrong.rDelimAst,"g").replace(/punct/g,w._punctuation).getRegex(),w.emStrong.rDelimUnd=p(w.emStrong.rDelimUnd,"g").replace(/punct/g,w._punctuation).getRegex(),w._escapes=/\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/g,w._scheme=/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/,w._email=/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/,w.autolink=p(w.autolink).replace("scheme",w._scheme).replace("email",w._email).getRegex(),w._attribute=/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/,w.tag=p(w.tag).replace("comment",w._comment).replace("attribute",w._attribute).getRegex(),w._label=/(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/,w._href=/<(?:\\.|[^\n<>\\])+>|[^\s\x00-\x1f]*/,w._title=/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/,w.link=p(w.link).replace("label",w._label).replace("href",w._href).replace("title",w._title).getRegex(),w.reflink=p(w.reflink).replace("label",w._label).replace("ref",B._label).getRegex(),w.nolink=p(w.nolink).replace("ref",B._label).getRegex(),w.reflinkSearch=p(w.reflinkSearch,"g").replace("reflink",w.reflink).replace("nolink",w.nolink).getRegex(),w.normal=F({},w),w.pedantic=F({},w.normal,{strong:{start:/^__|\*\*/,middle:/^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/,endAst:/\*\*(?!\*)/g,endUnd:/__(?!_)/g},em:{start:/^_|\*/,middle:/^()\*(?=\S)([\s\S]*?\S)\*(?!\*)|^_(?=\S)([\s\S]*?\S)_(?!_)/,endAst:/\*(?!\*)/g,endUnd:/_(?!_)/g},link:p(/^!?\[(label)\]\((.*?)\)/).replace("label",w._label).getRegex(),reflink:p(/^!?\[(label)\]\s*\[([^\]]*)\]/).replace("label",w._label).getRegex()}),w.gfm=F({},w.normal,{escape:p(w.escape).replace("])","~|])").getRegex(),_extended_email:/[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/,url:/^((?:ftp|https?):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/,_backpedal:/(?:[^?!.,:;*_'"~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_'"~)]+(?!$))+/,del:/^(~~?)(?=[^\s~])([\s\S]*?[^\s~])\1(?=[^~]|$)/,text:/^([`~]+|[^`~])(?:(?= {2,}\n)|(?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)|[\s\S]*?(?:(?=[\\'+(u?e:A(e,!0))+"
\n":""+(u?e:A(e,!0))+"
\n"},t.blockquote=function(e){return"\n"+e+" \n"},t.html=function(e){return e},t.heading=function(e,t,u,n){return this.options.headerIds?"\n":""+e+" \n"},t.hr=function(){return this.options.xhtml?" \n":" \n"},t.list=function(e,t,u){var n=t?"ol":"ul";return"<"+n+(t&&1!==u?' start="'+u+'"':"")+">\n"+e+""+n+">\n"},t.listitem=function(e){return""+e+" \n"},t.checkbox=function(e){return" "},t.paragraph=function(e){return" "+e+"
\n"},t.table=function(e,t){return"\n\n"+e+" \n"+(t=t&&""+t+" ")+"
\n"},t.tablerow=function(e){return"\n"+e+" \n"},t.tablecell=function(e,t){var u=t.header?"th":"td";return(t.align?"<"+u+' align="'+t.align+'">':"<"+u+">")+e+""+u+">\n"},t.strong=function(e){return""+e+" "},t.em=function(e){return""+e+" "},t.codespan=function(e){return""+e+"
"},t.br=function(){return this.options.xhtml?" ":" "},t.del=function(e){return""+e+""},t.link=function(e,t,u){return null===(e=f(this.options.sanitize,this.options.baseUrl,e))?u:(e='"+u+" ")},t.image=function(e,t,u){return null===(e=f(this.options.sanitize,this.options.baseUrl,e))?u:(e=' ":">"))},t.text=function(e){return e},e}(),z=function(){function e(){}var t=e.prototype;return t.strong=function(e){return e},t.em=function(e){return e},t.codespan=function(e){return e},t.del=function(e){return e},t.html=function(e){return e},t.text=function(e){return e},t.link=function(e,t,u){return""+u},t.image=function(e,t,u){return""+u},t.br=function(){return""},e}(),$=function(){function e(){this.seen={}}var t=e.prototype;return t.serialize=function(e){return e.toLowerCase().trim().replace(/<[!\/a-z].*?>/gi,"").replace(/[\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,./:;<=>?@[\]^`{|}~]/g,"").replace(/\s/g,"-")},t.getNextSafeSlug=function(e,t){var u=e,n=0;if(this.seen.hasOwnProperty(u))for(n=this.seen[e];u=e+"-"+ ++n,this.seen.hasOwnProperty(u););return t||(this.seen[e]=n,this.seen[u]=0),u},t.slug=function(e,t){void 0===t&&(t={});e=this.serialize(e);return this.getNextSafeSlug(e,t.dryrun)},e}(),S=function(){function u(e){this.options=e||r.defaults,this.options.renderer=this.options.renderer||new _,this.renderer=this.options.renderer,this.renderer.options=this.options,this.textRenderer=new z,this.slugger=new $}u.parse=function(e,t){return new u(t).parse(e)},u.parseInline=function(e,t){return new u(t).parseInline(e)};var e=u.prototype;return e.parse=function(e,t){void 0===t&&(t=!0);for(var u,n,r,i,s,l,o,a,D,c,h,p,f,g,F,A,k="",d=e.length,C=0;C",i?Promise.resolve(t):s?void s(null,t):t;if(i)return Promise.reject(e);if(!s)throw e;s(e)});if(null==e)return l(new Error("marked(): input parameter is undefined or null"));if("string"!=typeof e)return l(new Error("marked(): input parameter is of type "+Object.prototype.toString.call(e)+", string expected"));if((t=u)&&t.sanitize&&!t.silent&&console.warn("marked(): sanitize and sanitizer parameters are deprecated since version 0.7.0, should not be used and will be removed in the future. Read more here: https://marked.js.org/#/USING_ADVANCED.md#options"),u.hooks&&(u.hooks.options=u),n){var o,a=u.highlight;try{u.hooks&&(e=u.hooks.preprocess(e)),o=f(e,u)}catch(e){return l(e)}var D,c=function(t){var e;if(!t)try{u.walkTokens&&I.walkTokens(o,u.walkTokens),e=g(o,u),u.hooks&&(e=u.hooks.postprocess(e))}catch(e){t=e}return u.highlight=a,t?l(t):n(null,e)};return!a||a.length<3?c():(delete u.highlight,o.length?(D=0,I.walkTokens(o,function(u){"code"===u.type&&(D++,setTimeout(function(){a(u.text,u.lang,function(e,t){if(e)return c(e);null!=t&&t!==u.text&&(u.text=t,u.escaped=!0),0===--D&&c()})},0))}),void(0===D&&c())):c())}if(u.async)return Promise.resolve(u.hooks?u.hooks.preprocess(e):e).then(function(e){return f(e,u)}).then(function(e){return u.walkTokens?Promise.all(I.walkTokens(e,u.walkTokens)).then(function(){return e}):e}).then(function(e){return g(e,u)}).then(function(e){return u.hooks?u.hooks.postprocess(e):e}).catch(l);try{u.hooks&&(e=u.hooks.preprocess(e));var h=f(e,u),p=(u.walkTokens&&I.walkTokens(h,u.walkTokens),g(h,u));return p=u.hooks?u.hooks.postprocess(p):p}catch(e){return l(e)}}}function I(e,t,u){return R(v.lex,S.parse)(e,t,u)}T.passThroughHooks=new Set(["preprocess","postprocess"]),I.options=I.setOptions=function(e){return I.defaults=F({},I.defaults,e),e=I.defaults,r.defaults=e,I},I.getDefaults=e,I.defaults=r.defaults,I.use=function(){for(var D=I.defaults.extensions||{renderers:{},childTokens:{}},e=arguments.length,t=new Array(e),u=0;u {
64 | if (!response.ok) {
65 | throw new Error('Network response was not ok');
66 | }
67 | return response.text();
68 | })
69 | .then(content => {
70 | markdownEditor.value = content;
71 | updatePreview();
72 | saveState(); // This initializes the undo/redo stacks
73 | })
74 | .catch(error => {
75 | console.error('Error loading notepad content:', error);
76 | // If there's an error, we'll use whatever default content is already in the editor
77 | updatePreview();
78 | saveState();
79 | });
80 | }
81 |
82 | // Save content to the backend
83 | function saveContent() {
84 | if (!isDirty) return;
85 |
86 | const content = markdownEditor.value;
87 | fetch('/notepad/md.file', {
88 | method: 'POST',
89 | body: content,
90 | headers: {
91 | 'Content-Type': 'text/plain'
92 | }
93 | })
94 | .then(response => {
95 | if (!response.ok) {
96 | throw new Error('Network response was not ok');
97 | }
98 | lastSaveTime = Date.now();
99 | isDirty = false;
100 | console.log('Content saved successfully');
101 | })
102 | .catch(error => {
103 | console.error('Error saving content:', error);
104 | });
105 | }
106 |
107 | // Schedule an auto-save after inactivity
108 | function scheduleAutoSave() {
109 | // Clear any existing timeout
110 | if (saveTimeout) {
111 | clearTimeout(saveTimeout);
112 | }
113 |
114 | // Set a new timeout - save after 2 seconds of inactivity
115 | saveTimeout = setTimeout(() => {
116 | saveContent();
117 | }, 2000);
118 | }
119 |
120 | function updatePreview() {
121 | marked.setOptions({
122 | breaks: true,
123 | gfm: true,
124 | headerIds: true,
125 | highlight: function(code, language) {
126 | if (language && hljs.getLanguage(language)) {
127 | try {
128 | return hljs.highlight(code, { language: language }).value;
129 | } catch (err) {}
130 | }
131 | return hljs.highlightAuto(code).value;
132 | }
133 | });
134 | markdownPreview.innerHTML = marked.parse(markdownEditor.value);
135 | markdownPreview.querySelectorAll('pre code').forEach((block) => {
136 | hljs.highlightElement(block);
137 | });
138 | const links = markdownPreview.querySelectorAll('a');
139 | links.forEach(link => {
140 | link.setAttribute('target', '_blank');
141 | link.setAttribute('rel', 'noopener noreferrer');
142 | });
143 | }
144 |
145 | function decreaseFontSize() {
146 | if (baseFontSize > 10) {
147 | baseFontSize -= 1;
148 | updateFontSize();
149 | }
150 | }
151 |
152 | function increaseFontSize() {
153 | if (baseFontSize < 30) {
154 | baseFontSize += 1;
155 | updateFontSize();
156 | }
157 | }
158 |
159 | function updateFontSize() {
160 | markdownEditor.style.fontSize = `${baseFontSize}px`;
161 | markdownPreview.style.fontSize = `${baseFontSize}px`;
162 | const headings = markdownPreview.querySelectorAll('h1, h2, h3, h4, h5, h6');
163 | headings.forEach(heading => {
164 | const level = parseInt(heading.tagName.substring(1));
165 | const scaleFactor = Math.max(1 + (1.5 - level * 0.1), 1);
166 | heading.style.fontSize = `${baseFontSize * scaleFactor}px`;
167 | });
168 | const codeElements = markdownPreview.querySelectorAll('code');
169 | codeElements.forEach(code => {
170 | code.style.fontSize = `${baseFontSize * 0.9}px`;
171 | });
172 | }
173 |
174 | // Toggle between reader and writer mode
175 | function toggleMode() {
176 | isReaderMode = !isReaderMode;
177 | editorContainer.classList.toggle('reader-mode', isReaderMode);
178 | if (isReaderMode) {
179 | toggleModeBtn.innerHTML = ' ';
180 | toggleModeBtn.title = "Write Mode";
181 | } else {
182 | toggleModeBtn.innerHTML = ' ';
183 | toggleModeBtn.title = "Reader Mode";
184 | markdownEditor.focus();
185 | }
186 | updatePreview();
187 | }
188 |
189 | // Undo/Redo functionality
190 | function saveState() {
191 | const currentValue = markdownEditor.value;
192 | if (currentValue !== lastChange) {
193 | undoStack.push(lastChange);
194 | lastChange = currentValue;
195 | redoStack = [];
196 | if (undoStack.length > 100) {
197 | undoStack.shift();
198 | }
199 | }
200 | }
201 |
202 | function undoChange() {
203 | if (undoStack.length > 0) {
204 | const currentValue = markdownEditor.value;
205 | redoStack.push(currentValue);
206 | const previousValue = undoStack.pop();
207 | markdownEditor.value = previousValue;
208 | lastChange = previousValue;
209 | updatePreview();
210 | isDirty = true;
211 | scheduleAutoSave();
212 | }
213 | }
214 |
215 | function redoChange() {
216 | if (redoStack.length > 0) {
217 | const currentValue = markdownEditor.value;
218 | undoStack.push(currentValue);
219 | const nextValue = redoStack.pop();
220 | markdownEditor.value = nextValue;
221 | lastChange = nextValue;
222 | updatePreview();
223 | isDirty = true;
224 | scheduleAutoSave();
225 | }
226 | }
227 |
--------------------------------------------------------------------------------
/static/rtext.js:
--------------------------------------------------------------------------------
1 | // DOM Elements
2 | const noteEditor = document.getElementById('note-editor');
3 | const decreaseFontBtn = document.getElementById('decrease-font');
4 | const increaseFontBtn = document.getElementById('increase-font');
5 |
6 | // State Variables
7 | let lastCaretPosition = null;
8 | let baseFontSize = 16;
9 | let lastSaveTime = Date.now();
10 | let saveTimeout = null;
11 | let isDirty = false;
12 |
13 | // Initialize Application
14 | document.addEventListener('DOMContentLoaded', function() {
15 | loadContent();
16 | setupEventListeners();
17 | updateFontSize();
18 | });
19 |
20 | // Load content from the backend
21 | function loadContent() {
22 | fetch('/notepad/rtext.file')
23 | .then(response => {
24 | if (!response.ok) {
25 | throw new Error('Network response was not ok');
26 | }
27 | return response.text();
28 | })
29 | .then(content => {
30 | noteEditor.innerHTML = content;
31 | })
32 | .catch(error => {
33 | console.error('Error loading notepad content:', error);
34 | });
35 | }
36 |
37 | // Save content to the backend
38 | function saveContent() {
39 | if (!isDirty) return;
40 | const content = noteEditor.innerHTML;
41 | fetch('/notepad/rtext.file', {
42 | method: 'POST',
43 | body: content,
44 | headers: {
45 | 'Content-Type': 'text/plain'
46 | }
47 | })
48 | .then(response => {
49 | if (!response.ok) {
50 | throw new Error('Network response was not ok');
51 | }
52 | lastSaveTime = Date.now();
53 | isDirty = false;
54 | console.log('Content saved successfully');
55 | })
56 | .catch(error => {
57 | console.error('Error saving content:', error);
58 | });
59 | }
60 |
61 | // Schedule an auto-save after inactivity
62 | function scheduleAutoSave() {
63 | // Clear any existing timeout
64 | if (saveTimeout) {
65 | clearTimeout(saveTimeout);
66 | }
67 | saveTimeout = setTimeout(() => {
68 | saveContent();
69 | }, 2000);
70 | }
71 |
72 | // Basic Setup Functions
73 | function setupEventListeners() {
74 | // Caret position tracking
75 | noteEditor.addEventListener('mouseup', updateCaretPosition);
76 | noteEditor.addEventListener('keyup', updateCaretPosition);
77 | noteEditor.addEventListener('focus', updateCaretPosition);
78 | // Auto-save events
79 | noteEditor.addEventListener('input', function() {
80 | isDirty = true;
81 | scheduleAutoSave();
82 | });
83 | noteEditor.addEventListener('keyup', function() {
84 | scheduleAutoSave();
85 | });
86 | // Save before leaving the page
87 | window.addEventListener('beforeunload', function() {
88 | if (isDirty) {
89 | saveContent();
90 | }
91 | });
92 |
93 | // Font size controls
94 | decreaseFontBtn.addEventListener('click', decreaseFontSize);
95 | increaseFontBtn.addEventListener('click', increaseFontSize);
96 |
97 | // Handle link clicks
98 | noteEditor.addEventListener('click', function(event) {
99 | const target = event.target;
100 | if (target.tagName === 'A') {
101 | event.preventDefault();
102 | window.open(target.href, '_blank');
103 | }
104 | });
105 |
106 | // Handle paste event to always paste as plain text
107 | noteEditor.addEventListener('paste', function(e) {
108 | e.preventDefault();
109 | const text = (e.clipboardData || window.clipboardData).getData('text');
110 | if (text) {
111 | document.execCommand('insertText', false, text);
112 | isDirty = true;
113 | scheduleAutoSave();
114 | }
115 | });
116 | }
117 |
118 | // Editor Formatting Functions
119 | function formatDoc(cmd, value = null) {
120 | if (cmd === 'insertHorizontalRule') {
121 | document.execCommand(cmd, false, value);
122 | const selection = window.getSelection();
123 | if (selection.rangeCount > 0) {
124 | const range = selection.getRangeAt(0);
125 | let node = range.startContainer;
126 | // Navigate up to find the contenteditable element
127 | while (node && node !== noteEditor) {
128 | node = node.parentNode;
129 | }
130 | if (node) {
131 | // Find the most recently inserted HR element
132 | const hrs = node.querySelectorAll('hr');
133 | if (hrs.length > 0) {
134 | const lastHr = hrs[hrs.length - 1];
135 | lastHr.style.borderColor = '#a2c1f4';
136 | lastHr.style.backgroundColor = '#a2c1f4';
137 | lastHr.style.height = '1px';
138 | }
139 | }
140 | }
141 | } else {
142 | document.execCommand(cmd, false, value);
143 | }
144 | noteEditor.focus();
145 | isDirty = true;
146 | scheduleAutoSave();
147 | }
148 |
149 | function updateCaretPosition() {
150 | const sel = window.getSelection();
151 | if (sel.getRangeAt && sel.rangeCount) {
152 | lastCaretPosition = sel.getRangeAt(0);
153 | }
154 | }
155 |
156 | function createLink() {
157 | const selection = window.getSelection();
158 | if (!selection || selection.rangeCount === 0 || selection.toString().trim() === '') {
159 | alert('Please select some text first');
160 | return;
161 | }
162 | const range = selection.getRangeAt(0);
163 | const modal = document.createElement('div');
164 | modal.classList.add('modal');
165 | modal.innerHTML = `
166 |
167 |
Add Link
168 |
169 |
Add
170 |
Cancel
171 |
172 | `;
173 |
174 | document.body.appendChild(modal);
175 | const urlInput = modal.querySelector('#urlInput');
176 | urlInput.focus();
177 | urlInput.select();
178 |
179 | function handleConfirm() {
180 | const url = urlInput.value;
181 | if (url) {
182 | formatDoc('createLink', url);
183 | }
184 | modal.remove();
185 | }
186 |
187 | modal.querySelector('#confirm-link').addEventListener('click', handleConfirm);
188 | modal.querySelector('#cancel-link').addEventListener('click', function() {
189 | modal.remove();
190 | });
191 |
192 | modal.addEventListener('click', function(e) {
193 | if (e.target === modal) {
194 | modal.remove();
195 | }
196 | });
197 |
198 | modal.addEventListener('keydown', function(e) {
199 | if (e.key === 'Enter') {
200 | handleConfirm();
201 | }
202 | });
203 | }
204 |
205 | function insertCodeBlock() {
206 | const modal = document.createElement('div');
207 | modal.className = 'modal';
208 | modal.innerHTML = `
209 |
210 |
Add Code Block
211 |
212 |
Add
213 |
Cancel
214 |
215 | `;
216 |
217 | document.body.appendChild(modal);
218 | const textarea = modal.querySelector('textarea');
219 | textarea.focus();
220 |
221 | modal.querySelector('#confirm-code').addEventListener('click', function() {
222 | if (textarea.value.trim()) {
223 | const codeBlock = document.createElement('div');
224 | codeBlock.className = 'code-block';
225 | codeBlock.textContent = textarea.value;
226 | if (lastCaretPosition) {
227 | lastCaretPosition.deleteContents();
228 | lastCaretPosition.insertNode(codeBlock);
229 | } else {
230 | noteEditor.appendChild(codeBlock);
231 | }
232 | isDirty = true;
233 | scheduleAutoSave();
234 | }
235 | modal.remove();
236 | });
237 |
238 | modal.querySelector('#cancel-code').addEventListener('click', function() {
239 | modal.remove();
240 | });
241 | modal.addEventListener('click', function(e) {
242 | if (e.target === modal) {
243 | modal.remove();
244 | }
245 | });
246 | }
247 |
248 | function insertTable() {
249 | const modal = document.createElement('div');
250 | modal.className = 'modal';
251 | modal.innerHTML = `
252 |
253 |
Select Table Size
254 |
255 |
Cancel
256 |
257 | `;
258 |
259 | document.body.appendChild(modal);
260 | const grid = modal.querySelector('.grid');
261 | const MAX_SIZE = 10;
262 | let selectedRows = 0;
263 | let selectedCols = 0;
264 |
265 | // Create grid cells
266 | for (let i = 1; i <= MAX_SIZE; i++) {
267 | for (let j = 1; j <= MAX_SIZE; j++) {
268 | const cell = document.createElement('div');
269 | cell.className = 'cell';
270 | cell.dataset.row = i;
271 | cell.dataset.col = j;
272 | grid.appendChild(cell);
273 | }
274 | }
275 |
276 | // Handle cell hover
277 | grid.addEventListener('mouseover', function(e) {
278 | if (e.target.classList.contains('cell')) {
279 | selectedRows = +e.target.dataset.row;
280 | selectedCols = +e.target.dataset.col;
281 | highlightCells(selectedRows, selectedCols);
282 | }
283 | });
284 |
285 | // Handle cell click
286 | grid.addEventListener('click', function(e) {
287 | if (e.target.classList.contains('cell')) {
288 | insertTableHTML(selectedRows, selectedCols);
289 | modal.remove();
290 | }
291 | });
292 |
293 | // Highlight cells in grid
294 | function highlightCells(rows, cols) {
295 | document.querySelectorAll('.cell').forEach(cell => {
296 | const cellRow = +cell.dataset.row;
297 | const cellCol = +cell.dataset.col;
298 | cell.classList.toggle('selected', cellRow <= rows && cellCol <= cols);
299 | });
300 | }
301 |
302 | // Insert table into editor
303 | function insertTableHTML(rows, cols) {
304 | const ZERO_WIDTH_SPACE = '\u200B';
305 | let tableHtml = '';
306 |
307 | // Create header cells
308 | for (let j = 0; j < cols; j++) {
309 | tableHtml += `${ZERO_WIDTH_SPACE} `;
310 | }
311 | tableHtml += ' ';
312 |
313 | // Create rows and cells
314 | for (let i = 0; i < rows - 1; i++) {
315 | tableHtml += '';
316 | for (let j = 0; j < cols; j++) {
317 | tableHtml += `${ZERO_WIDTH_SPACE} `;
318 | }
319 | tableHtml += ' ';
320 | }
321 |
322 | tableHtml += '
';
323 | const tableElement = document.createElement('div');
324 | tableElement.innerHTML = tableHtml;
325 | if (lastCaretPosition) {
326 | lastCaretPosition.deleteContents();
327 | lastCaretPosition.insertNode(tableElement.firstChild);
328 | lastCaretPosition.collapse(false);
329 | } else {
330 | noteEditor.appendChild(tableElement.firstChild);
331 | }
332 |
333 | // Add resize handle to table
334 | const table = noteEditor.querySelector('table:last-of-type');
335 | table.style.position = 'relative';
336 | table.style.width = '100%';
337 |
338 | const resizeHandle = document.createElement('div');
339 | resizeHandle.className = 'resize-handle';
340 | resizeHandle.innerHTML = '↘';
341 | table.appendChild(resizeHandle);
342 |
343 | setupTableResizing(table, resizeHandle);
344 | isDirty = true;
345 | scheduleAutoSave();
346 | }
347 |
348 | modal.querySelector('#cancel-table').addEventListener('click', function() {
349 | modal.remove();
350 | });
351 | }
352 |
353 | // Table Resizing Functionality
354 | function setupTableResizing(table, handle) {
355 | let isResizing = false;
356 | let startX, startY;
357 | let startWidth, startHeight;
358 |
359 | handle.addEventListener('mousedown', startResize);
360 | function startResize(e) {
361 | e.preventDefault();
362 | e.stopPropagation();
363 |
364 | isResizing = true;
365 | startX = e.clientX;
366 | startY = e.clientY;
367 | startWidth = table.offsetWidth;
368 | startHeight = table.offsetHeight;
369 |
370 | document.addEventListener('mousemove', resize);
371 | document.addEventListener('mouseup', stopResize);
372 | }
373 |
374 | function resize(e) {
375 | if (!isResizing) return;
376 | const width = startWidth + (e.clientX - startX);
377 | const height = startHeight + (e.clientY - startY);
378 | if (width > 100) {
379 | table.style.width = `${width}px`;
380 | }
381 | if (height > 50) {
382 | table.style.height = `${height}px`;
383 | }
384 | }
385 |
386 | function stopResize() {
387 | isResizing = false;
388 | document.removeEventListener('mousemove', resize);
389 | document.removeEventListener('mouseup', stopResize);
390 | isDirty = true;
391 | scheduleAutoSave();
392 | }
393 | }
394 |
395 | // Font Size Functionality
396 | function decreaseFontSize() {
397 | if (baseFontSize > 10) {
398 | baseFontSize -= 1;
399 | updateFontSize();
400 | }
401 | }
402 |
403 | function increaseFontSize() {
404 | if (baseFontSize < 30) {
405 | baseFontSize += 1;
406 | updateFontSize();
407 | }
408 | }
409 |
410 | function updateFontSize() {
411 | noteEditor.style.fontSize = `${baseFontSize}px`;
412 | noteEditor.querySelectorAll("h1").forEach(h1 => {
413 | h1.style.fontSize = `${baseFontSize * 1.5}px`;
414 | });
415 | noteEditor.querySelectorAll("h2").forEach(h2 => {
416 | h2.style.fontSize = `${baseFontSize * 1.3}px`;
417 | });
418 | noteEditor.querySelectorAll("th, td").forEach(cell => {
419 | cell.style.fontSize = `${baseFontSize}px`;
420 | });
421 | noteEditor.querySelectorAll(".code-block").forEach(codeBlock => {
422 | codeBlock.style.fontSize = `${baseFontSize}px`;
423 | });
424 | }
425 |
426 | // Select All Text
427 | function selectAllText() {
428 | const selection = window.getSelection();
429 | const range = document.createRange();
430 | range.selectNodeContents(noteEditor);
431 | selection.removeAllRanges();
432 | selection.addRange(range);
433 | }
434 |
--------------------------------------------------------------------------------
/static/style.css:
--------------------------------------------------------------------------------
1 | :root {
2 | /* Color palette */
3 | --purple-primary: #7C3AED;
4 | --purple-light: #e2deed;
5 | --pink-secondary: #EC4899;
6 | --blue-accent: #4682B4;
7 | --blue-link: #a2c1f4;
8 |
9 | /* Neutrals - Light Mode */
10 | --white: #FFFFFF;
11 | --gray-100: #F1F2F4;
12 | --gray-200: #E4E6EA;
13 | --gray-300: #D0D4DA;
14 | --gray-500: #6A7179;
15 | --gray-700: #364050;
16 |
17 | /* Neutrals - Dark Mode */
18 | --dark-bg: #111827;
19 | --dark-card: #1F2937;
20 | --dark-hover: #303948;
21 | --dark-text: #E5E7EB;
22 | --dark-text-secondary: #9CA3AF;
23 | --dark-border: #303948;
24 | --light-custom-bg: #EAEBED;
25 |
26 | /* Semantic Variables - Light Mode Default */
27 | --primary: var(--purple-primary);
28 | --primary-light: var(--purple-light);
29 | --secondary: var(--pink-secondary);
30 | --bg: #F5F5F5;
31 |
32 | /* Component backgrounds */
33 | --bg-primary: var(--bg);
34 | --bg-secondary: var(--white);
35 | --bg-tertiary: var(--gray-100);
36 | --bg-button: var(--light-custom-bg);
37 |
38 | /* Text colors */
39 | --text-primary: var(--gray-700);
40 | --text-secondary: var(--gray-500);
41 | --text-accent: var(--primary);
42 | --text-muted: var(--gray-500);
43 | --text-link: var(--primary);
44 |
45 | /* Borders */
46 | --border-primary: var(--gray-200);
47 | --border-secondary: var(--gray-300);
48 |
49 | /* Interactive elements */
50 | --button-hover: var(--primary-light);
51 | --scrollbar-thumb: var(--gray-300);
52 | --code-bg: var(--gray-100);
53 | --table-header-bg: var(--gray-100);
54 | --selected-bg: var(--primary);
55 | --button-primary-bg: var(--primary);
56 | --button-primary-text: var(--white);
57 | --button-secondary-bg: var(--gray-100);
58 | --button-secondary-text: var(--gray-700);
59 | --grid-cell-bg: var(--gray-100);
60 | --grid-cell-border: var(--gray-300);
61 | }
62 |
63 | /* Dark Mode Override */
64 | @media (prefers-color-scheme: dark) {
65 | :root {
66 | --bg: var(--dark-bg);
67 | --white: var(--dark-card);
68 | --gray-100: var(--dark-hover);
69 | --gray-200: var(--dark-border);
70 | --gray-300: var(--dark-text-secondary);
71 | --gray-500: var(--dark-text-secondary);
72 | --gray-700: var(--dark-text);
73 |
74 | --bg-primary: var(--dark-bg);
75 | --bg-secondary: var(--dark-card);
76 | --bg-tertiary: var(--dark-hover);
77 | --bg-button: var(--gray-100);
78 |
79 | --text-primary: var(--dark-text);
80 | --text-secondary: var(--dark-text-secondary);
81 | --text-muted: var(--dark-text-secondary);
82 | --text-link: var(--primary-light);
83 |
84 | --border-primary: var(--dark-border);
85 | --border-secondary: var(--dark-text-secondary);
86 |
87 | --button-hover: var(--dark-hover);
88 | --scrollbar-thumb: var(--dark-text-secondary);
89 | --code-bg: rgba(0, 0, 0, 0.2);
90 | --table-header-bg: var(--dark-hover);
91 | }
92 | }
93 |
94 | /* Reset and Base Styles */
95 | * {
96 | margin: 0;
97 | padding: 0;
98 | box-sizing: border-box;
99 | }
100 |
101 | body {
102 | font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
103 | background: var(--bg);
104 | color: var(--text-primary);
105 | min-height: 100vh;
106 | }
107 |
108 | .container {
109 | max-width: 1000px;
110 | margin: 0 auto;
111 | padding: 3rem 2rem;
112 | }
113 |
114 | header {
115 | text-align: center;
116 | margin-bottom: 2rem;
117 | position: relative;
118 | }
119 |
120 | h1 {
121 | font-size: 2.5rem;
122 | font-weight: 800;
123 | background: linear-gradient(135deg, var(--primary), var(--secondary));
124 | background-clip: text;
125 | -webkit-background-clip: text;
126 | -webkit-text-fill-color: transparent;
127 | margin-bottom: 1rem;
128 | }
129 |
130 | /* Scrollbar Styling */
131 | ::-webkit-scrollbar {
132 | width: 8px;
133 | }
134 |
135 | ::-webkit-scrollbar-track {
136 | background: var(--bg-tertiary);
137 | border-radius: 4px;
138 | }
139 |
140 | ::-webkit-scrollbar-thumb {
141 | background: var(--scrollbar-thumb);
142 | border-radius: 4px;
143 | }
144 |
145 | ::-webkit-scrollbar-thumb:hover {
146 | background: var(--text-secondary);
147 | }
148 |
149 | /* Main Content and Container Styles */
150 | .input-section {
151 | display: flex;
152 | flex-direction: column;
153 | align-items: center;
154 | gap: 0.5rem;
155 | margin-bottom: 3rem;
156 | position: relative;
157 | }
158 |
159 | .compose-card {
160 | background: var(--bg-secondary);
161 | border-radius: 24px;
162 | padding: 1rem;
163 | box-shadow: 0 4px 20px rgba(124, 58, 237, 0.05);
164 | position: relative;
165 | width: 100%;
166 | max-width: 1000px;
167 | }
168 |
169 | @media (prefers-color-scheme: dark) {
170 | .compose-card {
171 | box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
172 | }
173 | }
174 |
175 | .buttons-container {
176 | display: flex;
177 | justify-content: center;
178 | gap: 10px;
179 | margin-bottom: 15px;
180 | }
181 |
182 | /* Form Elements */
183 | textarea {
184 | width: 100%;
185 | background: var(--bg-tertiary);
186 | border: 1px solid transparent;
187 | border-radius: 16px;
188 | padding: 1.25rem;
189 | font-size: 1rem;
190 | color: var(--text-primary);
191 | min-height: 120px;
192 | resize: vertical;
193 | transition: all 0.3s ease;
194 | font-family: inherit;
195 | }
196 |
197 | textarea:focus {
198 | outline: none;
199 | border-color: var(--primary-light);
200 | background: var(--bg-secondary);
201 | }
202 |
203 | textarea::placeholder {
204 | color: var(--text-secondary);
205 | }
206 |
207 | .filename-input {
208 | width: 100%;
209 | background: var(--bg-tertiary);
210 | border: 1px solid transparent;
211 | border-radius: 16px;
212 | padding: 0.75rem 1.25rem;
213 | font-size: 1rem;
214 | color: var(--text-primary);
215 | margin-bottom: 1rem;
216 | transition: all 0.3s ease;
217 | font-family: inherit;
218 | }
219 |
220 | .filename-input:focus {
221 | outline: none;
222 | border-color: var(--primary-light);
223 | background: var(--bg-secondary);
224 | }
225 |
226 | .filename-input::placeholder {
227 | color: var(--text-secondary);
228 | }
229 |
230 | /* Button Styles */
231 | .actions-wrapper {
232 | position: absolute;
233 | right: 2.5rem;
234 | bottom: 2.5rem;
235 | display: flex;
236 | gap: 1rem;
237 | }
238 |
239 | .btn-icon-small {
240 | padding: 8px 16px;
241 | background: var(--bg-tertiary);
242 | color: var(--text-secondary);
243 | border-radius: 12px;
244 | transition: all 0.2s ease;
245 | cursor: pointer;
246 | font-size: 0.95rem;
247 | width: 32px;
248 | height: 32px;
249 | display: flex;
250 | align-items: center;
251 | justify-content: center;
252 | gap: 0.5rem;
253 | border: none;
254 | }
255 |
256 | .btn-icon-small:hover {
257 | background: var(--primary-light);
258 | color: var(--primary);
259 | }
260 |
261 | .btn-icon {
262 | padding: 8px 16px;
263 | background: var(--bg-button);
264 | color: var(--text-secondary);
265 | border-radius: 12px;
266 | transition: all 0.2s ease;
267 | cursor: pointer;
268 | font-size: 0.95rem;
269 | min-width: 32px;
270 | height: 32px;
271 | display: flex;
272 | align-items: center;
273 | justify-content: center;
274 | gap: 0.5rem;
275 | border: none;
276 | }
277 |
278 | .btn-icon:hover {
279 | background: var(--primary-light);
280 | color: var(--primary);
281 | }
282 |
283 | .upload-text {
284 | font-weight: 500;
285 | color: var(--text-secondary);
286 | }
287 |
288 | .btn-icon:hover .upload-text {
289 | color: var(--primary);
290 | }
291 |
292 | .btn-secondary {
293 | background: var(--bg-secondary);
294 | color: var(--test-secondary);
295 | }
296 |
297 | .btn-secondary:hover {
298 | background: var(--bg-tertiary);
299 | }
300 |
301 | .back-btn {
302 | margin-right: 10px;
303 | background-color: var(--primary);
304 | color: var(--white);
305 | border: none;
306 | border-radius: 6px;
307 | padding: 5px 10px;
308 | cursor: pointer;
309 | display: flex;
310 | align-items: center;
311 | gap: 5px;
312 | font-size: 0.9rem;
313 | }
314 |
315 | .back-btn:hover {
316 | background-color: #6D28D9;
317 | }
318 |
319 | .action-button {
320 | display: flex;
321 | align-items: center;
322 | gap: 8px;
323 | padding: 8px 16px;
324 | border-radius: 8px;
325 | cursor: pointer;
326 | font-size: 14px;
327 | font-weight: 500;
328 | border: none;
329 | transition: all 0.2s ease;
330 | }
331 |
332 | .primary-button {
333 | background: var(--primary);
334 | color: var(--white);
335 | }
336 |
337 | .primary-button:hover {
338 | background: #6D28D9;
339 | }
340 |
341 | .secondary-button {
342 | background: var(--gray-200);
343 | color: var(--text-primary);
344 | }
345 |
346 | .secondary-button:hover {
347 | background: var(--gray-300);
348 | }
349 |
350 | /* File Upload Styling */
351 | input[type="file"] {
352 | display: none;
353 | }
354 |
355 | .file-upload-form {
356 | text-align: center;
357 | }
358 |
359 | .drag-over {
360 | border: 2px dashed var(--primary);
361 | background: var(--primary-light);
362 | }
363 |
364 | /* Content Sections */
365 | .content-sections {
366 | display: flex;
367 | flex-direction: column;
368 | gap: 2rem;
369 | }
370 |
371 | .content-section {
372 | border-radius: 24px;
373 | }
374 |
375 | .section-title {
376 | font-size: 1.25rem;
377 | font-weight: 600;
378 | color: var(--text-primary);
379 | margin-bottom: 1rem;
380 | padding-bottom: 0.75rem;
381 | text-align: center;
382 | border-bottom: 1px solid var(--border-primary);
383 | }
384 |
385 | .entries-grid,
386 | .entries-grid-files {
387 | display: grid;
388 | gap: 1rem;
389 | grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
390 | }
391 |
392 | .entry-card {
393 | background: var(--bg-secondary);
394 | border-radius: 20px;
395 | padding: 1rem;
396 | transition: all 0.3s ease;
397 | position: relative;
398 | overflow: hidden;
399 | }
400 |
401 | .entry-card:hover {
402 | box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
403 | }
404 |
405 | @media (prefers-color-scheme: dark) {
406 | .entry-card:hover {
407 | box-shadow: 0 8px 24px rgba(90, 90, 90, 0.1);
408 | }
409 | }
410 |
411 | .entry-content {
412 | font-size: 0.95rem;
413 | color: var(--text-secondary);
414 | margin-bottom: 1rem;
415 | }
416 |
417 | .entry-actions {
418 | display: flex;
419 | gap: 0.75rem;
420 | justify-content: flex-end;
421 | }
422 |
423 | .entry-actions .btn-icon {
424 | padding: 16px;
425 | color: var(--text-secondary);
426 | background: var(--bg-tertiary);
427 | }
428 |
429 | .entry-actions .btn-icon:hover {
430 | background: var(--primary-light);
431 | color: var(--primary);
432 | }
433 |
434 | /* Content Viewer Styles */
435 | .viewer-container {
436 | padding: 20px;
437 | max-width: 1000px;
438 | margin: 0 auto;
439 | }
440 |
441 | .viewer-actions {
442 | display: flex;
443 | justify-content: space-between;
444 | margin-bottom: 20px;
445 | gap: 10px;
446 | }
447 |
448 | .viewer-filename {
449 | font-size: 1.2rem;
450 | font-weight: 600;
451 | margin-bottom: 15px;
452 | color: var(--text-primary);
453 | text-align: center;
454 | }
455 |
456 | .content-view {
457 | background: var(--bg-secondary);
458 | padding: 20px;
459 | border-radius: 12px;
460 | white-space: pre-wrap;
461 | word-break: break-word;
462 | line-height: 1.5;
463 | font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
464 | color: var(--text-primary);
465 | box-shadow: 0 4px 20px rgba(124, 58, 237, 0.05);
466 | }
467 |
468 | @media (prefers-color-scheme: dark) {
469 | .content-view {
470 | box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
471 | }
472 | }
473 |
474 | /* Notepad Styles (Both MD and Rich Text) */
475 | .note-container {
476 | display: flex;
477 | flex-direction: column;
478 | height: 100vh;
479 | width: 100%;
480 | max-width: 1200px;
481 | margin: 0 auto;
482 | padding: 20px;
483 | }
484 |
485 | .note-header {
486 | display: flex;
487 | justify-content: center;
488 | align-items: center;
489 | padding-bottom: 15px;
490 | margin-bottom: 20px;
491 | gap: 0.75rem;
492 | }
493 |
494 | .app-title {
495 | font-size: 2.5rem;
496 | font-weight: bold;
497 | background: linear-gradient(135deg, var(--primary), var(--secondary));
498 | background-clip: text;
499 | -webkit-background-clip: text;
500 | -webkit-text-fill-color: transparent;
501 | margin-bottom: 15px;
502 | text-align: center;
503 | }
504 |
505 | .note-content {
506 | display: flex;
507 | flex-direction: column;
508 | flex: 1;
509 | position: relative;
510 | }
511 |
512 | #created-date {
513 | color: var(--text-secondary);
514 | font-size: 0.85rem;
515 | margin-bottom: 15px;
516 | text-align: right;
517 | }
518 |
519 | /* Editor Toolbar */
520 | .editor-toolbar {
521 | display: flex;
522 | flex-wrap: wrap;
523 | gap: 5px;
524 | background-color: var(--bg-secondary);
525 | padding: 10px;
526 | border-radius: 12px;
527 | justify-content: center;
528 | overflow-x: auto;
529 | white-space: nowrap;
530 | margin-bottom: 10px;
531 | }
532 |
533 | .editor-toolbar button {
534 | background-color: transparent;
535 | border: none;
536 | color: var(--text-primary);
537 | font-size: 1rem;
538 | cursor: pointer;
539 | padding: 8px;
540 | border-radius: 4px;
541 | transition: background-color 0.2s;
542 | min-width: 32px;
543 | height: 32px;
544 | display: flex;
545 | align-items: center;
546 | justify-content: center;
547 | }
548 |
549 | .editor-toolbar button:hover {
550 | background-color: var(--button-hover);
551 | }
552 |
553 | .editor-toolbar button i {
554 | width: 16px;
555 | height: 16px;
556 | display: flex;
557 | align-items: center;
558 | justify-content: center;
559 | }
560 |
561 | /* Editor Container */
562 | #editor-container {
563 | flex: 1;
564 | display: flex;
565 | overflow: hidden;
566 | min-height: 300px;
567 | margin-bottom: 20px;
568 | }
569 |
570 | /* Markdown Editor */
571 | #markdown-editor {
572 | flex: 1;
573 | width: 100%;
574 | height: 100%;
575 | padding: 15px;
576 | background-color: var(--bg-secondary);
577 | color: var(--text-primary);
578 | border-radius: 12px;
579 | resize: none;
580 | font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
581 | font-size: 16px;
582 | line-height: 1.6;
583 | overflow-y: auto;
584 | box-shadow: 0 4px 20px rgba(124, 58, 237, 0.05);
585 | }
586 |
587 | /* Markdown Preview */
588 | #markdown-preview {
589 | display: none;
590 | flex: 1;
591 | padding: 15px;
592 | border-radius: 12px;
593 | background-color: var(--bg-secondary);
594 | overflow-y: auto;
595 | color: var(--text-primary);
596 | }
597 |
598 | /* Reader mode class */
599 | .reader-mode #markdown-editor {
600 | display: none;
601 | }
602 |
603 | .reader-mode #markdown-preview {
604 | display: block;
605 | }
606 |
607 | /* Rich Text Editor */
608 | .rich-text-editor {
609 | flex: 1;
610 | padding: 15px;
611 | border: 1px solid var(--border-primary);
612 | border-radius: 12px;
613 | background-color: var(--bg-secondary);
614 | min-height: 300px;
615 | color: var(--text-primary);
616 | box-shadow: 0 4px 20px rgba(124, 58, 237, 0.05);
617 | overflow-y: auto;
618 | }
619 |
620 | .rich-text-editor table {
621 | border-collapse: collapse;
622 | width: 100%;
623 | margin-bottom: 1em;
624 | }
625 |
626 | .rich-text-editor th,
627 | .rich-text-editor td {
628 | border: 1px solid var(--border-secondary);
629 | padding: 0.5em;
630 | }
631 |
632 | .rich-text-editor th {
633 | background-color: var(--table-header-bg);
634 | }
635 |
636 | /* Markdown Preview Styles */
637 | .markdown-body h1 {
638 | font-size: 1.8em;
639 | margin-top: 0.5em;
640 | margin-bottom: 0.5em;
641 | font-weight: bold;
642 | color: var(--text-accent);
643 | padding-bottom: 0.3em;
644 | border-bottom: 1px solid var(--border-primary);
645 | }
646 |
647 | .markdown-body h2 {
648 | font-size: 1.5em;
649 | margin-top: 0.5em;
650 | margin-bottom: 0.5em;
651 | font-weight: bold;
652 | color: var(--text-accent);
653 | padding-bottom: 0.3em;
654 | border-bottom: 1px solid var(--border-primary);
655 | }
656 |
657 | .markdown-body h3,
658 | .markdown-body h4,
659 | .markdown-body h5,
660 | .markdown-body h6 {
661 | margin-top: 0.5em;
662 | margin-bottom: 0.5em;
663 | font-weight: bold;
664 | color: var(--text-accent);
665 | }
666 |
667 | .markdown-body p {
668 | margin-bottom: 1em;
669 | }
670 |
671 | .markdown-body a {
672 | color: var(--text-link);
673 | text-decoration: none;
674 | }
675 |
676 | .markdown-body a:hover {
677 | text-decoration: underline;
678 | }
679 |
680 | .markdown-body img {
681 | max-width: 100%;
682 | }
683 |
684 | .markdown-body blockquote {
685 | border-left: 4px solid var(--primary);
686 | padding-left: 1em;
687 | margin-left: 0;
688 | margin-right: 0;
689 | color: var(--text-secondary);
690 | }
691 |
692 | .markdown-body table {
693 | border-collapse: collapse;
694 | width: 100%;
695 | margin-bottom: 1em;
696 | }
697 |
698 | .markdown-body th,
699 | .markdown-body td {
700 | border: 1px solid var(--border-secondary);
701 | padding: 0.5em;
702 | }
703 |
704 | .markdown-body th {
705 | background-color: var(--table-header-bg);
706 | }
707 |
708 | /* Code Block */
709 | .code-block {
710 | background-color: var(--code-bg);
711 | border-radius: 5px;
712 | padding: 15px;
713 | margin-bottom: 15px;
714 | font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
715 | color: var(--text-primary);
716 | white-space: pre-wrap;
717 | overflow-x: auto;
718 | border: 1px solid var(--border-secondary);
719 | }
720 |
721 | .markdown-body ul,
722 | .markdown-body ol {
723 | padding-left: 2em;
724 | }
725 |
726 | .markdown-body hr {
727 | margin-top: 1.5em;
728 | margin-bottom: 1.5em;
729 | border: 0;
730 | height: 1px;
731 | background-color: var(--border-secondary);
732 | }
733 |
734 | /* Base styles for code blocks */
735 | .markdown-body pre {
736 | background-color: var(--code-bg);
737 | border-radius: 8px;
738 | padding: 1em;
739 | overflow-x: auto;
740 | margin: 1.5em 0;
741 | }
742 |
743 | .markdown-body code {
744 | font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
745 | font-size: 0.9em;
746 | border-radius: 4px;
747 | }
748 |
749 | /* Inline code styling */
750 | .markdown-body :not(pre) > code {
751 | padding: 0.2em 0.4em;
752 | background-color: var(--code-bg);
753 | }
754 |
755 | @media (prefers-color-scheme: dark) {
756 | .markdown-body pre {
757 | background-color: #1e1e2e;
758 | }
759 | }
760 |
761 | @media (prefers-color-scheme: light) {
762 | .markdown-body pre {
763 | background-color: #eff1f5;
764 | }
765 | }
766 |
767 | /* Heading Buttons */
768 | .h1-button,
769 | .h2-button {
770 | display: flex;
771 | align-items: center;
772 | justify-content: center;
773 | }
774 |
775 | .h1-button i,
776 | .h2-button i {
777 | margin-right: 2px;
778 | }
779 |
780 | /* Modal styles */
781 | .modal {
782 | position: fixed;
783 | top: 0;
784 | left: 0;
785 | width: 100%;
786 | height: 100%;
787 | background-color: rgba(0, 0, 0, 0.5);
788 | display: flex;
789 | justify-content: center;
790 | align-items: center;
791 | z-index: 9999;
792 | }
793 |
794 | .modal-content {
795 | background-color: var(--bg-secondary);
796 | padding: 20px;
797 | border-radius: 12px;
798 | max-width: 500px;
799 | width: 90%;
800 | }
801 |
802 | .modal-content p {
803 | margin-bottom: 15px;
804 | }
805 |
806 | .modal-content input,
807 | .modal-content textarea {
808 | width: 100%;
809 | padding: 8px;
810 | background-color: var(--bg-tertiary);
811 | color: var(--text-primary);
812 | border-radius: 8px;
813 | }
814 |
815 | .modal-content input:focus,
816 | .modal-content textarea:focus {
817 | outline: none;
818 | border-color: var(--primary-light);
819 | }
820 |
821 | .modal-content button {
822 | padding: 8px 15px;
823 | margin-right: 10px;
824 | border: none;
825 | border-radius: 8px;
826 | cursor: pointer;
827 | }
828 |
829 | .modal-content button:first-of-type {
830 | background-color: var(--button-primary-bg);
831 | color: var(--button-primary-text);
832 | }
833 |
834 | .modal-content button:last-of-type {
835 | background-color: var(--button-secondary-bg);
836 | color: var(--button-secondary-text);
837 | border: 1px solid var(--border-primary);
838 | }
839 |
840 | /* Grid for table selection */
841 | .grid {
842 | display: grid;
843 | grid-template-columns: repeat(10, 20px);
844 | grid-template-rows: repeat(10, 20px);
845 | gap: 0;
846 | margin: 10px auto;
847 | justify-content: center;
848 | margin-bottom: 20px;
849 | }
850 |
851 | .cell {
852 | width: 20px;
853 | height: 20px;
854 | background: var(--grid-cell-bg);
855 | border: 1px solid var(--grid-cell-border);
856 | cursor: pointer;
857 | }
858 |
859 | .cell.selected {
860 | background: var(--selected-bg);
861 | }
862 |
863 | .resize-handle {
864 | position: absolute;
865 | right: -10px;
866 | bottom: -10px;
867 | width: 20px;
868 | height: 20px;
869 | cursor: se-resize;
870 | z-index: 1000;
871 | color: var(--text-primary);
872 | text-align: center;
873 | line-height: 20px;
874 | }
875 |
876 | /* Responsive styles */
877 | @media screen and (max-width: 768px) {
878 | h1 {
879 | font-size: 2rem;
880 | }
881 |
882 | .note-container {
883 | padding: 10px;
884 | }
885 |
886 | .editor-toolbar {
887 | overflow-x: auto;
888 | padding: 8px 5px;
889 | }
890 |
891 | .editor-toolbar button {
892 | padding: 6px;
893 | min-width: 28px;
894 | height: 28px;
895 | }
896 |
897 | .viewer-actions {
898 | flex-direction: column;
899 | }
900 |
901 | .action-button {
902 | width: 100%;
903 | justify-content: center;
904 | }
905 |
906 | .content-view {
907 | padding: 15px;
908 | }
909 |
910 | .app-title {
911 | font-size: 2rem;
912 | }
913 | }
914 |
--------------------------------------------------------------------------------
/static/sw.js:
--------------------------------------------------------------------------------
1 | self.addEventListener('install', (event) => {
2 | self.skipWaiting();
3 | });
4 |
5 | self.addEventListener('activate', (event) => {
6 | event.waitUntil(clients.claim());
7 | });
8 |
--------------------------------------------------------------------------------
/templates/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Local-Content-Share
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
53 |
54 |
55 |
56 |
57 | Local-Content-Share
58 |
59 |
60 |
61 |
114 |
115 |
116 |
117 |
118 | Snippets
119 |
120 | {{range .}}
121 | {{if eq .Type "text"}}
122 |
123 |
124 |
125 |
{{.Filename}}
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 | {{end}}
142 | {{end}}
143 |
144 |
145 |
146 |
147 |
148 | Files
149 |
150 | {{range .}}
151 | {{if eq .Type "file"}}
152 |
153 |
154 |
155 |
{{.Filename}}
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 | {{end}}
172 | {{end}}
173 |
174 |
175 |
176 |
177 |
178 |
277 |
308 |
343 |
392 |
393 |
394 |
--------------------------------------------------------------------------------
/templates/md.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Notepad
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
Notepad
18 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/templates/rtext.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Notepad
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
Notepad
15 |
23 |
24 |
25 |
26 | 1
27 | 2
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
Welcome to Rich Text Notepad
52 |
Start typing here to create your document. Use the toolbar above to format your text.
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/templates/show.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Content View
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
{{.Filename}}
15 |
23 |
{{.Content}}
24 |
25 |
65 |
66 |
67 |
--------------------------------------------------------------------------------