├── .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 |
2 | Local Content Share Logo 3 |

Local Content Share

4 | 5 | Build Workflow Container Workflow
6 | GitHub Release Docker Pulls

7 | ScreenshotsInstall & UseTips & NotesAcknowledgements 8 |
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 | Light | Light | 37 | | Dark | Dark | 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]*?(?:[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|\\n*|$)|\\n*|$)|)[\\s\\S]*?(?:(?:\\n *)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$)|(?=[ \\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",")|<(?: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",")|<(?: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",")|<(?:script|pre|style|textarea|!--)").replace("tag",B._tag).getRegex(),B.pedantic=F({},B.normal,{html:p("^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+? *(?:\\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:/^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\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-]*(?: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?"'+e+"\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"},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+"\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=''+u+'":">"))},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 | 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 | 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 | 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 += ``; 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 += ``; 318 | } 319 | tableHtml += ''; 320 | } 321 | 322 | tableHtml += '
    ${ZERO_WIDTH_SPACE}
    ${ZERO_WIDTH_SPACE}
    '; 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 |
    62 | 63 |
    64 | 65 |
    66 | 67 | 68 | 73 |
    74 | 75 | 76 | 80 | 81 | 82 | 86 | 87 | 88 | 89 | 90 | Notepad 91 | 92 |
    93 | 94 | 95 | 103 | 104 | 105 |
    106 |
    107 | 108 | 109 | 110 | 111 |
    112 |
    113 |
    114 | 115 |
    116 | 117 |
    118 |

    Snippets

    119 |
    120 | {{range .}} 121 | {{if eq .Type "text"}} 122 |
    123 | 124 | 125 |
    {{.Filename}}
    126 |
    127 | 130 | 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 | 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 |
    19 | 22 | 25 |
    26 | 27 |
    28 |
    29 | 30 | 31 | 32 | 33 | 34 |
    35 | 36 |
    37 | 38 |
    39 |
    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 |
    16 | 19 | 22 |
    23 | 24 |
    25 |
    26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 |
    48 | 49 |
    50 |
    51 |

    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 |
    16 | 19 | 22 |
    23 |
    {{.Content}}
    24 |
    25 | 65 | 66 | 67 | --------------------------------------------------------------------------------