Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a5fcf0213 | ||
|
|
3f34a02559 | ||
|
|
a5b67e7657 | ||
|
|
00a44a8e92 | ||
|
|
51b8dddac2 | ||
|
|
d4aece7f1e | ||
|
|
12373e9745 | ||
|
|
06fadcba92 | ||
|
|
81a8dd3307 | ||
|
|
f5c3dc8887 | ||
|
|
18d9b19f9f | ||
|
|
047ebfcff9 | ||
|
|
2ceb83177c | ||
|
|
ff4d7e12a5 | ||
|
|
9bad148649 | ||
|
|
25b02bc60e | ||
|
|
be3ca9c8a2 | ||
|
|
8c34e2b87c | ||
|
|
b54fdb7f98 | ||
|
|
8ad3a9e185 |
No files matched your search
@@ -0,0 +1,409 @@
|
||||
name: Build Artifacts
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
publish_release:
|
||||
description: "Publish a GitHub release"
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
release_tag:
|
||||
description: "Release tag or version label (required if publish_release=true)"
|
||||
required: false
|
||||
type: string
|
||||
release_name:
|
||||
description: "Release name override"
|
||||
required: false
|
||||
type: string
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
# ── Go server binaries (6 platform combos) ──────────────────────────────
|
||||
|
||||
build-server:
|
||||
name: Server ${{ matrix.goos }}-${{ matrix.goarch }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- goos: windows
|
||||
goarch: amd64
|
||||
- goos: windows
|
||||
goarch: arm64
|
||||
- goos: linux
|
||||
goarch: amd64
|
||||
- goos: linux
|
||||
goarch: arm64
|
||||
- goos: darwin
|
||||
goarch: amd64
|
||||
- goos: darwin
|
||||
goarch: arm64
|
||||
|
||||
env:
|
||||
GOOS: ${{ matrix.goos }}
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
CGO_ENABLED: 0
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.24.x"
|
||||
cache: true
|
||||
|
||||
- run: go mod download
|
||||
|
||||
- name: Build
|
||||
shell: pwsh
|
||||
run: |
|
||||
$out = "rizin-notebook-${{ matrix.goos }}-${{ matrix.goarch }}"
|
||||
$version = (git rev-parse --short HEAD).Trim()
|
||||
go build -o $out -ldflags "-X main.NBVERSION=$version"
|
||||
if ($env:GOOS -eq "windows") {
|
||||
Rename-Item -Path $out -NewName "$out.exe"
|
||||
}
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: rizin-notebook-${{ matrix.goos }}-${{ matrix.goarch }}
|
||||
path: rizin-notebook-${{ matrix.goos }}-${{ matrix.goarch }}*
|
||||
if-no-files-found: error
|
||||
|
||||
# ── Rizin core plugin (rizin-notebook-plugin) — Windows ─────────────────
|
||||
|
||||
build-rizin-plugin-windows:
|
||||
name: rizin-notebook-plugin (windows-x64)
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Verify plugin sources
|
||||
shell: pwsh
|
||||
run: |
|
||||
if (!(Test-Path "rizin-notebook-plugin\CMakeLists.txt")) {
|
||||
Write-Host "::error::rizin-notebook-plugin sources are missing from repository checkout. Commit the rizin-notebook-plugin directory."
|
||||
Get-ChildItem -Path .
|
||||
exit 1
|
||||
}
|
||||
|
||||
- name: Install vcpkg packages
|
||||
run: vcpkg install protobuf-c:x64-windows
|
||||
|
||||
- name: Download rizin SDK
|
||||
shell: pwsh
|
||||
run: |
|
||||
Invoke-WebRequest -Uri "https://github.com/rizinorg/rizin/releases/download/v0.8.2/rizin-windows-shared64-v0.8.2.zip" `
|
||||
-OutFile rizin.zip
|
||||
7z x rizin.zip -orizin-sdk
|
||||
$cfg = Get-ChildItem -Path rizin-sdk -Recurse -Filter "RizinConfig.cmake" |
|
||||
Select-Object -First 1
|
||||
if (!$cfg) {
|
||||
Write-Host "::error::RizinConfig.cmake not found in rizin SDK"
|
||||
Get-ChildItem -Path rizin-sdk -Recurse | Select-Object -First 80 |
|
||||
ForEach-Object { Write-Host $_.FullName }
|
||||
exit 1
|
||||
}
|
||||
$prefix = $cfg.Directory.Parent.Parent.Parent.FullName
|
||||
Write-Host "Rizin prefix: $prefix"
|
||||
Add-Content -Path $env:GITHUB_ENV -Value "RIZIN_PREFIX=$prefix"
|
||||
|
||||
- name: Configure
|
||||
shell: pwsh
|
||||
run: |
|
||||
cmake -S rizin-notebook-plugin -B rizin-notebook-plugin/build `
|
||||
"-DRizin_ROOT=$env:RIZIN_PREFIX" `
|
||||
"-DCMAKE_TOOLCHAIN_FILE=$env:VCPKG_INSTALLATION_ROOT\scripts\buildsystems\vcpkg.cmake" `
|
||||
-G "Visual Studio 17 2022" -A x64
|
||||
|
||||
- name: Build
|
||||
run: cmake --build rizin-notebook-plugin/build --config Release
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: rizin-notebook-plugin-windows-x64
|
||||
path: rizin-notebook-plugin/build/Release/rz_notebook.dll
|
||||
if-no-files-found: error
|
||||
|
||||
# ── Rizin core plugin (rizin-notebook-plugin) — Linux ───────────────────
|
||||
|
||||
build-rizin-plugin-linux:
|
||||
name: rizin-notebook-plugin (linux-x64)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Verify plugin sources
|
||||
run: |
|
||||
test -f rizin-notebook-plugin/CMakeLists.txt || {
|
||||
echo "::error::rizin-notebook-plugin sources are missing from repository checkout. Commit the rizin-notebook-plugin directory.";
|
||||
ls -la;
|
||||
exit 1;
|
||||
}
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y meson ninja-build pkg-config \
|
||||
libprotobuf-c-dev libcurl4-openssl-dev
|
||||
|
||||
- name: Build and install rizin
|
||||
run: |
|
||||
curl -L -o rizin-src.tar.xz \
|
||||
"https://github.com/rizinorg/rizin/releases/download/v0.8.2/rizin-src-v0.8.2.tar.xz"
|
||||
mkdir rizin-sdk-src
|
||||
tar xf rizin-src.tar.xz -C rizin-sdk-src --strip-components=1
|
||||
cd rizin-sdk-src
|
||||
meson setup build --prefix=/usr -Ddefault_library=shared
|
||||
ninja -C build
|
||||
sudo ninja -C build install
|
||||
sudo ldconfig
|
||||
|
||||
- name: Configure and build
|
||||
run: |
|
||||
cmake -S rizin-notebook-plugin -B rizin-notebook-plugin/build -DCMAKE_BUILD_TYPE=Release
|
||||
cmake --build rizin-notebook-plugin/build
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: rizin-notebook-plugin-linux-x64
|
||||
path: rizin-notebook-plugin/build/librz_notebook.so
|
||||
if-no-files-found: error
|
||||
|
||||
# ── Cutter plugin (cutter-notebook-plugin) — Windows ──────────────────
|
||||
|
||||
build-cutter-plugin-windows:
|
||||
name: cutter-notebook-plugin (windows-x64)
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Verify plugin sources
|
||||
shell: pwsh
|
||||
run: |
|
||||
if (!(Test-Path "cutter-notebook-plugin\CMakeLists.txt")) {
|
||||
Write-Host "::error::cutter-notebook-plugin sources are missing from repository checkout."
|
||||
Get-ChildItem -Path .
|
||||
exit 1
|
||||
}
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install Qt SDK
|
||||
shell: pwsh
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install aqtinstall
|
||||
$qtOut = Join-Path $env:RUNNER_TEMP "Qt"
|
||||
python -m aqt install-qt windows desktop 6.7.2 win64_msvc2019_64 --outputdir $qtOut
|
||||
$qtDir = Join-Path $qtOut "6.7.2\msvc2019_64"
|
||||
if (!(Test-Path $qtDir)) {
|
||||
Write-Host "::error::Qt SDK install failed: $qtDir not found"
|
||||
exit 1
|
||||
}
|
||||
Add-Content -Path $env:GITHUB_ENV -Value "QT_DIR=$qtDir"
|
||||
|
||||
- name: Download Cutter SDK
|
||||
shell: pwsh
|
||||
run: |
|
||||
Invoke-WebRequest -Uri "https://github.com/rizinorg/cutter/releases/download/v2.4.1/Cutter-v2.4.1-Windows-x86_64.zip" `
|
||||
-OutFile cutter.zip
|
||||
7z x cutter.zip -oCutter
|
||||
$cutterRoot = Get-ChildItem -Path Cutter -Directory | Select-Object -First 1
|
||||
if (!$cutterRoot) {
|
||||
Write-Host "::error::Failed to extract Cutter SDK"
|
||||
exit 1
|
||||
}
|
||||
$hdr = Join-Path $cutterRoot.FullName "include\cutter\plugins\CutterPlugin.h"
|
||||
if (!(Test-Path $hdr)) {
|
||||
Write-Host "::error::Invalid Cutter SDK root: $($cutterRoot.FullName)"
|
||||
exit 1
|
||||
}
|
||||
Add-Content -Path $env:GITHUB_ENV -Value "CUTTER_DIR=$($cutterRoot.FullName)"
|
||||
|
||||
- name: Configure
|
||||
shell: pwsh
|
||||
run: |
|
||||
cmake -S cutter-notebook-plugin -B cutter-notebook-plugin/build `
|
||||
"-DCutter_DIR=$env:CUTTER_DIR" `
|
||||
"-DCMAKE_PREFIX_PATH=$env:QT_DIR" `
|
||||
-G "Visual Studio 17 2022" -A x64
|
||||
|
||||
- name: Build
|
||||
run: cmake --build cutter-notebook-plugin/build --config Release
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: cutter-notebook-plugin-windows-x64
|
||||
path: |
|
||||
cutter-notebook-plugin/build/Release/CutterNotebookPlugin.dll
|
||||
cutter-notebook-plugin/build/Release/CutterNotebookPlugin.pdb
|
||||
if-no-files-found: error
|
||||
|
||||
# ── Package plugin sources ─────────────────────────────────────────────
|
||||
|
||||
package-sources:
|
||||
name: Package plugin sources
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Verify source directories
|
||||
run: |
|
||||
test -d rizin-notebook-plugin || { echo "::error::Missing rizin-notebook-plugin directory"; exit 1; }
|
||||
test -d cutter-notebook-plugin || { echo "::error::Missing cutter-notebook-plugin directory"; exit 1; }
|
||||
|
||||
- name: Create archives
|
||||
run: |
|
||||
zip -r rizin-notebook-plugin-source.zip rizin-notebook-plugin proto
|
||||
zip -r cutter-notebook-plugin-source.zip cutter-notebook-plugin
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: plugin-sources
|
||||
path: |
|
||||
rizin-notebook-plugin-source.zip
|
||||
cutter-notebook-plugin-source.zip
|
||||
if-no-files-found: error
|
||||
|
||||
# ── Package checksums manifest ───────────────────────────────────────
|
||||
|
||||
package-checksums:
|
||||
name: Package checksums
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- build-server
|
||||
- build-rizin-plugin-windows
|
||||
- build-rizin-plugin-linux
|
||||
- build-cutter-plugin-windows
|
||||
- package-sources
|
||||
steps:
|
||||
- uses: actions/download-artifact@v4
|
||||
|
||||
- name: Generate SHA256SUMS
|
||||
shell: pwsh
|
||||
run: |
|
||||
$files = Get-ChildItem -Path . -Recurse -File |
|
||||
Where-Object { $_.Name -ne 'SHA256SUMS.txt' } |
|
||||
Sort-Object FullName
|
||||
|
||||
$lines = foreach ($file in $files) {
|
||||
$hash = (Get-FileHash -Path $file.FullName -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
$relative = [System.IO.Path]::GetRelativePath((Get-Location).Path, $file.FullName) -replace '\\', '/'
|
||||
"$hash $relative"
|
||||
}
|
||||
|
||||
$lines | Set-Content -Path SHA256SUMS.txt -Encoding utf8
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: release-checksums
|
||||
path: SHA256SUMS.txt
|
||||
if-no-files-found: error
|
||||
|
||||
# ── Publish GitHub Release ─────────────────────────────────────────────
|
||||
|
||||
release:
|
||||
name: Publish Release
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- build-server
|
||||
- build-rizin-plugin-windows
|
||||
- build-rizin-plugin-linux
|
||||
- build-cutter-plugin-windows
|
||||
- package-sources
|
||||
- package-checksums
|
||||
if: >-
|
||||
startsWith(github.ref, 'refs/tags/v') ||
|
||||
(github.event_name == 'workflow_dispatch' && inputs.publish_release)
|
||||
steps:
|
||||
- uses: actions/download-artifact@v4
|
||||
|
||||
- name: Resolve tag and name
|
||||
id: meta
|
||||
shell: pwsh
|
||||
env:
|
||||
RAW_RELEASE_TAG: ${{ inputs.release_tag }}
|
||||
RAW_RELEASE_NAME: ${{ inputs.release_name }}
|
||||
run: |
|
||||
$tag = ""
|
||||
$name = ""
|
||||
$targetCommitish = ""
|
||||
|
||||
if ($env:GITHUB_EVENT_NAME -eq "workflow_dispatch") {
|
||||
$requestedTag = [string]$env:RAW_RELEASE_TAG
|
||||
$requestedTag = $requestedTag.Trim()
|
||||
if ([string]::IsNullOrWhiteSpace($requestedTag)) {
|
||||
Write-Error "release_tag is required when publish_release=true"
|
||||
exit 1
|
||||
}
|
||||
|
||||
$name = ([string]$env:RAW_RELEASE_NAME).Trim()
|
||||
if ([string]::IsNullOrWhiteSpace($name)) {
|
||||
$name = $requestedTag
|
||||
}
|
||||
|
||||
$normalizedTag = $requestedTag
|
||||
if (-not $normalizedTag.StartsWith("v")) {
|
||||
$normalizedTag = "v$normalizedTag"
|
||||
}
|
||||
|
||||
$normalizedTag = [regex]::Replace($normalizedTag.Trim(), "\s+", "-")
|
||||
$normalizedTag = [regex]::Replace($normalizedTag, "[^0-9A-Za-z._/-]", "-")
|
||||
$normalizedTag = [regex]::Replace($normalizedTag, "-{2,}", "-")
|
||||
$normalizedTag = $normalizedTag.Trim('-')
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($normalizedTag) -or $normalizedTag -eq "v") {
|
||||
Write-Error "release_tag could not be normalized into a valid Git tag"
|
||||
exit 1
|
||||
}
|
||||
|
||||
$tag = $normalizedTag
|
||||
$targetCommitish = $env:GITHUB_SHA
|
||||
}
|
||||
else {
|
||||
$tag = $env:GITHUB_REF_NAME
|
||||
$name = $tag
|
||||
}
|
||||
|
||||
$prerelease = $false
|
||||
if ($tag -match "(?i)(alpha|beta|rc|preview|pre)" -or $name -match "(?i)(alpha|beta|rc|preview|pre)") {
|
||||
$prerelease = $true
|
||||
}
|
||||
|
||||
"tag=$tag" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
|
||||
"name=$name" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
|
||||
"target_commitish=$targetCommitish" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
|
||||
"prerelease=$($prerelease.ToString().ToLowerInvariant())" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
|
||||
|
||||
- uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag_name: ${{ steps.meta.outputs.tag }}
|
||||
name: ${{ steps.meta.outputs.name }}
|
||||
target_commitish: ${{ steps.meta.outputs.target_commitish }}
|
||||
generate_release_notes: true
|
||||
draft: false
|
||||
prerelease: ${{ steps.meta.outputs.prerelease == 'true' }}
|
||||
files: |
|
||||
rizin-notebook-linux-*/*
|
||||
rizin-notebook-windows-*/*
|
||||
rizin-notebook-darwin-*/*
|
||||
rizin-notebook-plugin-*/*
|
||||
cutter-notebook-plugin-*/*
|
||||
plugin-sources/*
|
||||
release-checksums/*
|
||||
@@ -0,0 +1,28 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.24.x"
|
||||
cache: true
|
||||
|
||||
- run: go mod download
|
||||
- run: go vet ./...
|
||||
- run: go build -o rizin-notebook -ldflags "-X main.NBVERSION=$(git rev-parse --short HEAD)"
|
||||
@@ -1,2 +1,47 @@
|
||||
rizin-notebook
|
||||
.test-data
|
||||
rizin-notebook.exe
|
||||
rizin-notebook.exe~
|
||||
build-local/
|
||||
build-local-*/
|
||||
*.test
|
||||
*.out
|
||||
coverage.out
|
||||
.test-data
|
||||
.rizin-notebook/
|
||||
**/build/
|
||||
**/build-*/
|
||||
**/cmake-build-*/
|
||||
**/CMakeFiles/
|
||||
**/CMakeCache.txt
|
||||
**/cmake_install.cmake
|
||||
**/compile_commands.json
|
||||
**/Makefile
|
||||
**/*.sln
|
||||
**/*.vcxproj
|
||||
**/*.vcxproj.filters
|
||||
**/*.vcxproj.user
|
||||
**/*.dir/
|
||||
**/*.tlog
|
||||
*.obj
|
||||
*.o
|
||||
*.a
|
||||
*.lib
|
||||
*.dll
|
||||
*.dylib
|
||||
*.so
|
||||
*.exp
|
||||
*.pdb
|
||||
.env
|
||||
.env.*
|
||||
.vscode/
|
||||
.vs/
|
||||
cutter-notebook-plugin/build/
|
||||
cutter-notebook-plugin/cmake-build-*/
|
||||
cutter-notebook-plugin/.qt/
|
||||
cutter-notebook-plugin/*.zip
|
||||
rizin-notebook-plugin/build/
|
||||
rizin-notebook-plugin/cmake-build-*/
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
embedded-rizin-check.txt
|
||||
embedded-rizin-plugins.txt
|
||||
|
Before Width: | Height: | Size: 101 KiB |
@@ -1,23 +1,150 @@
|
||||
# Rizin Notebook
|
||||
# Rizin Notebook
|
||||
|
||||
A notebook to write notes while using `rizin`.
|
||||
A notebook for binary analysis with rizin. Keep notes, commands, and scripts organized per binary.
|
||||
|
||||
If you want to compare it with something similar, you can call it as the rizin equivalent of [jupyter notebook](https://jupyter.org/)
|
||||
Requires rizin 0.7.0 or later.
|
||||
|
||||
## Requirements
|
||||
## What it does
|
||||
|
||||
Requires at least rizin version `0.2.0`
|
||||
Each notebook **page** is tied to one binary. It stores the binary itself, its SHA-256 hash,
|
||||
and a list of **cells**:
|
||||
|
||||
## Screenshot
|
||||
- **command cells** - a rizin command and its output
|
||||
- **script cells** - a JavaScript snippet run on the server against a rizin pipe
|
||||
- **markdown cells** - freeform notes
|
||||
|
||||

|
||||
Pages are stored as individual `.rznb` files (SQLite databases) in `~/.rizin-notebook/`.
|
||||
They can be exported and imported as single self-contained files.
|
||||
|
||||
## Building
|
||||
## Components
|
||||
|
||||
- **Go server** (`rizin-notebook`) - the main application. Serves a web UI, exposes a
|
||||
protobuf + JSON REST API, stores everything in per-page `.rznb` SQLite files.
|
||||
- **rizin-notebook-plugin** - a rizin core plugin (C). Adds `NB` commands to rizin so you
|
||||
can interact with the notebook from the rizin shell.
|
||||
- **cutter-notebook-plugin** - a Cutter plugin (C++/Qt6). Adds a Plugins -> Notebook dock
|
||||
and menu to Cutter.
|
||||
|
||||
The plugins are optional. The Go server works standalone with just a browser.
|
||||
|
||||
## Compatibility note
|
||||
|
||||
The Go server is platform-independent. The plugins depend on the Rizin version they are
|
||||
built against.
|
||||
|
||||
If standalone rizin and Cutter ship different Rizin versions on your machine, build
|
||||
`rz_notebook` once for each. Both builds can talk to the same server and share the same
|
||||
`.rznb` data.
|
||||
|
||||
Simple rule:
|
||||
|
||||
- one server
|
||||
- one `rz_notebook` build per Rizin version in use
|
||||
- one Cutter plugin if you want the Cutter dock
|
||||
|
||||
## Screenshots
|
||||
|
||||
Project screenshots are organized under `docs/images/`.
|
||||
|
||||
### Web UI
|
||||
|
||||
<figure>
|
||||
<img src="docs/images/web-homepage.png" alt="Rizin Notebook" width="100%" />
|
||||
<figcaption><em>Figure 1. Homepage view (with cell & scope based search, supporting multiple modes.)</em></figcaption>
|
||||
</figure>
|
||||
|
||||
<figure>
|
||||
<img src="docs/images/web-settings.png" alt="Rizin Notebook settings page" width="100%" />
|
||||
<figcaption><em>Figure 2. Settings page for environment configuration, keyboard shortcuts, and server-side behavior.</em></figcaption>
|
||||
</figure>
|
||||
|
||||
<figure>
|
||||
<img src="docs/images/web-autocomplete.png" alt="Autocomplete suggestions for Rizin commands in the notebook web UI" width="100%" />
|
||||
<figcaption><em>Figure 3. Command autocomplete in the web console, helping discover and complete Rizin commands quickly.</em></figcaption>
|
||||
</figure>
|
||||
|
||||
<figure>
|
||||
<img src="docs/images/web-command-help.png" alt="Command search and help modal in the notebook web UI" width="100%" />
|
||||
<figcaption><em>Figure 4. Built-in command help and search modal for exploring command descriptions and arguments.</em></figcaption>
|
||||
</figure>
|
||||
|
||||
### Plugin Integration
|
||||
|
||||
<figure>
|
||||
<img src="docs/images/rizin-plugin.png" alt="Rizin core plugin integration for Rizin Notebook" width="100%" />
|
||||
<figcaption><em>Figure 5. Rizin core plugin integration, enabling notebook interaction directly from the Rizin shell.</em></figcaption>
|
||||
</figure>
|
||||
|
||||
<figure>
|
||||
<img src="docs/images/cutter-plugin-1.png" alt="Cutter plugin integration screenshot 1" width="100%" />
|
||||
<figcaption><em>Figure 6. Cutter plugin integration view showing the notebook experience inside the Cutter interface.</em></figcaption>
|
||||
</figure>
|
||||
|
||||
<figure>
|
||||
<img src="docs/images/cutter-plugin-2.png" alt="Cutter plugin integration screenshot 2" width="100%" />
|
||||
<figcaption><em>Figure 7. Additional Cutter plugin workflow view demonstrating possible notebook usage(s).</em></figcaption>
|
||||
</figure>
|
||||
|
||||
## Building the server
|
||||
|
||||
```bash
|
||||
go get -v github.com/gin-gonic/gin
|
||||
go get -v github.com/jessevdk/go-assets-builder
|
||||
go build -o rizin-notebook .
|
||||
```
|
||||
|
||||
go-assets-builder assets -o assets.go
|
||||
go build -ldflags "-X main.NBVERSION=$(git rev-list -1 HEAD)"
|
||||
```
|
||||
Embed the version string during release builds:
|
||||
|
||||
```bash
|
||||
go build -ldflags "-X main.NBVERSION=$(git rev-parse --short HEAD)" -o rizin-notebook .
|
||||
```
|
||||
|
||||
## Running
|
||||
|
||||
```bash
|
||||
./rizin-notebook
|
||||
```
|
||||
|
||||
Opens at `http://127.0.0.1:8000/` by default.
|
||||
|
||||
Flags:
|
||||
|
||||
```
|
||||
-bind 127.0.0.1:8000 address and port to listen on
|
||||
-root / URL path prefix
|
||||
-notebook /path/to/data data directory (default: ~/.rizin-notebook)
|
||||
-debug-assets /path load templates from disk instead of embedded
|
||||
-debug enable HTTP request logs
|
||||
```
|
||||
|
||||
Set `RIZIN_PATH` to override the rizin binary path used for pipe execution.
|
||||
|
||||
## Building the rizin plugin
|
||||
|
||||
See [rizin-notebook-plugin/README.md](rizin-notebook-plugin/README.md).
|
||||
|
||||
## Building the Cutter plugin
|
||||
|
||||
See [cutter-notebook-plugin/README.md](cutter-notebook-plugin/README.md).
|
||||
|
||||
If Cutter ships its own embedded Rizin, the `rz_notebook` plugin loaded by Cutter must be
|
||||
built against that same bundled Rizin version.
|
||||
|
||||
## Data storage
|
||||
|
||||
Each page is a `.rznb` file in the data directory. It is a SQLite database containing:
|
||||
|
||||
- page metadata (title, original filename, binary storage key, SHA-256 hash)
|
||||
- cells (type, content, output, timestamps)
|
||||
- the binary blob itself
|
||||
- per-page config (key/value)
|
||||
|
||||
The server maintains a `catalog.db` index for fast listing across all pages.
|
||||
|
||||
On first run the server automatically migrates any pages from an older monolithic
|
||||
`notebook.db` format into the per-page `.rznb` layout.
|
||||
|
||||
## API
|
||||
|
||||
All `NB` plugin commands use the protobuf API (`Content-Type: application/x-protobuf`).
|
||||
The schema is in `proto/notebook.proto`.
|
||||
|
||||
A parallel JSON API at `/api/v1/json/...` is used by the Cutter plugin and the browser UI.
|
||||
@@ -1,164 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/jessevdk/go-assets"
|
||||
)
|
||||
|
||||
var _Assetsccaa111ee439fc8a890b718441ba429bfa47c126 = "{{ $root := .root }}{{ $page := .page }}{{ $pipe := .pipe }}<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\">\n <title>Rizin Notebook</title>\n <link rel=\"stylesheet\" href=\"{{ $root }}static/spectre-icons.min.css\">\n <link rel=\"stylesheet\" href=\"{{ $root }}static/spectre.min.css\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <style type=\"text/css\">\n .spacing-top { margin: 3px 0px 0px 0px; }\n .spacing-left { margin: 0px 3px 0px 0px; }\n .align-center { text-align: center; }\n .navbar-fixed-height { height: 32px; }\n .icon-height { height: 20px; }\n .title-margin { margin-top: auto; margin-bottom: auto; margin-right: 10px; }\n .md-heigh { height: 64px; }\n .pipe { color: #e85600; border-color: #e85600; }\n .pipe:hover { background: #e85600; border-color: #e85600; }\n iframe { border: 0px; width: 100%; height: 100% }\n .resizer { display: flex; margin: 0; padding: 0; resize: vertical; overflow: hidden }\n .resizer > .resized { flex-grow: 1; margin: 0; padding: 0; border: 0 }\n .syntax-menu { right: 35px; }\n .align-right { text-align: right; }\n </style>\n <script>\n function handle(obj) {\n obj.style.height = (obj.contentWindow.document.documentElement.scrollHeight + 20) + 'px';\n if (obj.contentWindow.location.href.indexOf(\"/deleted\") > 0) {\n obj.parentNode.parentNode.removeChild(obj.parentNode);\n }\n window.scrollTo(0, document.body.scrollHeight);\n }\n function output(obj) {\n obj.style.height = obj.contentWindow.document.documentElement.scrollHeight + 'px';\n if (obj.contentWindow.location.href.indexOf(\"/deleted\") > 0) {\n obj.parentNode.parentNode.removeChild(obj.parentNode);\n } else if (obj.contentWindow.location.href.indexOf(\"/loaded\") > 0) {\n location.reload()\n }\n window.scrollTo(0, document.body.scrollHeight);\n }\n function newmd() {\n var body = document.getElementById(\"page-body\");\n var div = document.createElement(\"div\");\n var iframe = document.createElement(\"iframe\");\n\n div.className = \"panel spacing-top resizer\";\n iframe.className = \"panel-body no-borders\";\n iframe.setAttribute(\"frameborder\", \"0\");\n iframe.setAttribute(\"marginheight\", \"0\");\n iframe.setAttribute(\"marginwidth\", \"0\");\n iframe.setAttribute(\"width\", \"100%\");\n iframe.setAttribute(\"height\", \"100%\");\n iframe.setAttribute(\"scrolling\", \"no\");\n iframe.setAttribute(\"onload\", \"handle(this)\");\n iframe.src = \"{{ $root }}markdown/new/{{ $page.unique }}\";\n div.appendChild(iframe);\n body.insertBefore(div, body.childNodes[body.childNodes.length - 2]);\n }\n function newcm() {\n var body = document.getElementById(\"page-body\");\n var div = document.createElement(\"div\");\n var iframe = document.createElement(\"iframe\");\n\n div.className = \"panel spacing-top\";\n iframe.className = \"panel-body no-borders\";\n iframe.setAttribute(\"frameborder\", \"0\");\n iframe.setAttribute(\"marginheight\", \"0\");\n iframe.setAttribute(\"marginwidth\", \"0\");\n iframe.setAttribute(\"width\", \"100%\");\n iframe.setAttribute(\"height\", \"100%\");\n iframe.setAttribute(\"scrolling\", \"no\");\n iframe.setAttribute(\"onload\", \"output(this)\");\n iframe.src = \"{{ $root }}output/input/{{ $page.unique }}\";\n div.appendChild(iframe);\n body.insertBefore(div, body.childNodes[body.childNodes.length - 2]);\n }\n function confirm_delete(event) {\n if (!window.confirm(\"Are you sure you want to delete the section?\")) {\n event.preventDefault();\n }\n }\n </script>\n </head>\n<body>\n <section class=\"container grid-960 mt-10\">\n <header class=\"navbar navbar-fixed-height\">\n <section class=\"navbar-section\">\n <img src=\"{{ $root }}static/logo.png\" width=\"30\" height=\"30\" /> \n <a href=\"{{ $root }}\" class=\"navbar-brand mr-10\">Rizin Notebook</a>\n </section>\n <section class=\"navbar-section\">\n <h4 class=\"title-margin\">{{ $page.title }}</h4>\n \n <a href=\"{{ $root }}pipe/{{ if $pipe }}close{{ else }}open{{ end }}/{{ $page.unique }}\" class=\"btn{{ if $pipe }} pipe{{ end }}\">\n {{ if $pipe }}Close Pipe{{ else }}Open Pipe{{ end }}\n </a>\n \n <a href=\"{{ $root }}\" class=\"btn btn-primary\">Go Back</a>\n </section>\n </header>\n <section class=\"container\">\n <div class=\"columns\">\n <div class=\"column\">\n <div class=\"input-group spaLine truncated
|
||||
var _Assetsce3dea9f2eb66eda07e7b51153e713391edd7f4c = "/*! jQuery v3.2.1 -ajax,-ajax/jsonp,-ajax/load,-ajax/parseXML,-ajax/script,-ajax/var/location,-ajax/var/nonce,-ajax/var/rquery,-ajax/xhr,-manipulation/_evalUrl,-event/ajax,-effects,-effects/Tween,-effects/animatedSelector | (c) JS Foundation and other contributors | jquery.org/license */\n!function(a,b){\"use strict\";\"object\"==typeof module&&\"object\"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error(\"jQuery requires a window with a document\");return b(a)}:b(a)}(\"undefined\"!=typeof window?window:this,function(a,b){\"use strict\";var c=[],d=a.document,e=Object.getPrototypeOf,f=c.slice,g=c.concat,h=c.push,i=c.indexOf,j={},k=j.toString,l=j.hasOwnProperty,m=l.toString,n=m.call(Object),o={};function p(a,b){b=b||d;var c=b.createElement(\"script\");c.text=a,b.head.appendChild(c).parentNode.removeChild(c)}var q=\"3.2.1 -ajax,-ajax/jsonp,-ajax/load,-ajax/parseXML,-ajax/script,-ajax/var/location,-ajax/var/nonce,-ajax/var/rquery,-ajax/xhr,-manipulation/_evalUrl,-event/ajax,-effects,-effects/Tween,-effects/animatedSelector\",r=function(a,b){return new r.fn.init(a,b)},s=/^[\\s\\uFEFF\\xA0]+|[\\s\\uFEFF\\xA0]+$/g,t=/^-ms-/,u=/-([a-z])/g,v=function(a,b){return b.toUpperCase()};r.fn=r.prototype={jquery:q,constructor:r,length:0,toArray:function(){return f.call(this)},get:function(a){return null==a?f.call(this):a<0?this[a+this.length]:this[a]},pushStack:function(a){var b=r.merge(this.constructor(),a);return b.prevObject=this,b},each:function(a){return r.each(this,a)},map:function(a){return this.pushStack(r.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(f.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(a<0?b:0);return this.pushStack(c>=0&&c<b?[this[c]]:[])},end:function(){return this.prevObject||this.constructor()},push:h,sort:c.sort,splice:c.splice},r.extend=r.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for(\"boolean\"==typeof g&&(j=g,g=arguments[h]||{},h++),\"object\"==typeof g||r.isFunction(g)||(g={}),h===i&&(g=this,h--);h<i;h++)if(null!=(a=arguments[h]))for(b in a)c=g[b],d=a[b],g!==d&&(j&&d&&(r.isPlainObject(d)||(e=Array.isArray(d)))?(e?(e=!1,f=c&&Array.isArray(c)?c:[]):f=c&&r.isPlainObject(c)?c:{},g[b]=r.extend(j,f,d)):void 0!==d&&(g[b]=d));return g},r.extend({expando:\"jQuery\"+(q+Math.random()).replace(/\\D/g,\"\"),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return\"function\"===r.type(a)},isWindow:function(a){return null!=a&&a===a.window},isNumeric:function(a){var b=r.type(a);return(\"number\"===b||\"string\"===b)&&!isNaN(a-parseFloat(a))},isPlainObject:function(a){var b,c;return!(!a||\"[object Object]\"!==k.call(a))&&(!(b=e(a))||(c=l.call(b,\"constructor\")&&b.constructor,\"function\"==typeof c&&m.call(c)===n))},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},type:function(a){return null==a?a+\"\":\"object\"==typeof a||\"function\"==typeof a?j[k.call(a)]||\"object\":typeof a},globalEval:function(a){p(a)},camelCase:function(a){return a.replace(t,\"ms-\").replace(u,v)},each:function(a,b){var c,d=0;if(w(a)){for(c=a.length;d<c;d++)if(b.call(a[d],d,a[d])===!1)break}else for(d in a)if(b.call(a[d],d,a[d])===!1)break;return a},trim:function(a){return null==a?\"\":(a+\"\").replace(s,\"\")},makeArray:function(a,b){var c=b||[];return null!=a&&(w(Object(a))?r.merge(c,\"string\"==typeof a?[a]:a):h.call(c,a)),c},inArray:function(a,b,c){return null==b?-1:i.call(b,a,c)},merge:function(a,b){for(var c=+b.length,d=0,e=a.length;d<c;d++)a[e++]=b[d];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;f<g;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,e,f=0,h=[];if(w(a))for(d=a.length;f<d;f++)e=b(a[f],f,c),null!=e&&h.push(e);else for(f in a)e=b(a[f],f,c),null!=e&&h.push(e);return g.apply([],h)},guid:1,proxy:function(a,b){var c,d,e;if(\"string\"==typeof b&&(c=a[b],b=a,a=c),r.isFunction(a))return d=f.call(arguments,2),e=function(){return a.apply(b||this,d.concat(f.call(arguments)))},e.guid=a.guid=a.guid||r.guid++,e},now:Date.now,support:o}),\"function\"==typeof Symbol&&(r.fn[Symbol.iterator]=c[Symbol.iterator]),r.each(\"Boolean Number String Function Array Date RegExp Object Error Symbol\".split(\" \"),function(a,b){j[\"[object \"+b+\"]\"]=b.toLowerCase()});function w(a){var b=!!a&&\"length\"in a&&a.length,c=r.type(a);return\"function\"!==c&&!r.isWindow(a)&&(\"array\"===c||0===b||\"number\"==typeof b&&b>0&&b-1 in a)}var x=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u=\"sizzle\"+1*new Date,v=a.document,w=0,x=0,y=ha(),z=ha(),A=ha(),B=function(a,b){return a===b&&(l=!0),0},C={}.hasOwnProperty,D=[],E=D.pop,F=D.push,G=D.push,H=D.slice,I=function(a,b){for(var c=0,d=a.length;c<d;c++)if(a[c]===b)return c;return-1},J=\"checked|selected|async|autofocus|autopLine truncated
|
||||
var _Assetsa73f174e8addbc8adb067ef8cce595d0ae511369 = "{{ $root := .root }}<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"utf-8\">\n <title>Rizin Notebook</title>\n <link rel=\"stylesheet\" href=\"{{ $root }}static/spectre-icons.min.css\">\n <link rel=\"stylesheet\" href=\"{{ $root }}static/spectre.min.css\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <style type=\"text/css\">\n .spacing-top { margin: 3px 0px 0px 0px; }\n .spacing-left { margin: 0px 3px 0px 0px; }\n .align-center { text-align: center; }\n .pipe { color: #e85600; border-color: #e85600; }\n .pipe:hover { background: #e85600; border-color: #e85600; }\n .footer { position: absolute; bottom: 0px; padding: 5px; }\n </style>\n</head>\n<body>\n <section class=\"container grid-960 mt-10\">\n <header class=\"navbar\">\n <section class=\"navbar-section\">\n <img src=\"{{ $root }}static/logo.png\" width=\"30\" height=\"30\" /> \n <a href=\"{{ $root }}\" class=\"navbar-brand mr-10\">Rizin Notebook</a>\n </section>\n <section class=\"navbar-section\">\n <a href=\"{{ $root }}settings\" class=\"btn\">Settings</a>\n \n <a href=\"{{ $root }}about\" class=\"btn\">About</a>\n \n <a href=\"{{ $root }}new\" class=\"btn btn-primary\">New Page</a>\n </section>\n </header>\n <section class=\"container\">\n <div class=\"columns\">\n <div class=\"column\">\n {{ $length := len .list }}{{ if eq $length 0 }}\n <div class=\"input-group spacing-top\">\n <span class=\"input-group-addon align-center\">To start, create a new page.</span>\n </div>\n {{ else }}{{ range $i, $page := .list }}\n <div class=\"input-group spacing-top\">\n <span class=\"input-group-addon spacing-left\">{{ $page.title }}</span>\n {{ if $page.pipe }}\n <button class=\"btn tooltip tooltip-bottom pipe spacing-left\" data-tooltip=\"The pipe is still open.\"><i class=\"icon icon-more-horiz\"></i></button>\n {{ else }}\n <a class=\"btn spacing-left\" href=\"{{ $root }}edit/{{ $page.unique }}\">\n <i class=\"icon icon-edit\">Edit</i>\n </a>\n {{ end }}\n <a class=\"btn spacing-left\" href=\"{{ $root }}view/{{ $page.unique }}\">\n <i class=\"icon icon-forward\">View</i>\n </a>\n </div>\n {{ end }}{{ end }}\n </div>\n </div>\n </section>\n </section>\n</body>\n</html>"
|
||||
var _Assets92ed65f24ecf383f92d30c1d5413460a473ae582 = "<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"utf-8\">\n <title>Rizin Notebook</title>\n <link rel=\"stylesheet\" href=\"{{ .root }}static/spectre-icons.min.css\">\n <link rel=\"stylesheet\" href=\"{{ .root }}static/spectre.min.css\">\n <link rel=\"stylesheet\" href=\"{{ .root }}static/simplemde.min.css\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <style type=\"text/css\">\n html, body { width: 100%; height: 100%; margin: 0 }\n </style>\n</head>\n<body>\n <div style=\"margin-top: 15px; height: 100%;\">\n <div class=\"form-group\">\n <input type=\"text\" disabled=\"\" class=\"form-input\" value=\"Error: {{ .error }}\"/>\n </div>\n <div class=\"form-group\">\n <a class=\"btn\" href=\"{{ .root }}output/deleted\">Delete</a>\n </div>\n </div>\n</body>\n</html>"
|
||||
var _Assets25265b46fc5005cf89577b3590eb083ef336b559 = "<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"utf-8\">\n <title>Rizin Notebook</title>\n <link rel=\"stylesheet\" href=\"{{ .root }}static/spectre-icons.min.css\">\n <link rel=\"stylesheet\" href=\"{{ .root }}static/spectre.min.css\">\n <link rel=\"stylesheet\" href=\"{{ .root }}static/simplemde.min.css\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <style type=\"text/css\">\n html, body { width: 100%; height: 100%; margin: 0 }\n .position-right { position: absolute; right: 0px; z-index: 99999; }\n .button-edit { width: 75px ! important; margin-top: 9px; margin-right: 9px; }\n </style>\n</head>\n<body onload=\"editor()\">\n <div style=\"margin-top: 15px; height: 100%;\">\n <form action=\"{{ .root }}markdown/save{{ .path }}\" method=\"POST\">\n <div class=\"position-right\">\n <button class=\"btn btn-action button-edit\" type=\"submit\"><i class=\"icon icon-share\">Code</i> Save</button>\n <a class=\"btn btn-action button-edit\" href=\"{{ .root }}markdown/delete{{ .path }}\"><i class=\"icon icon-cross\">Code</i> Delete</a>\n <a class=\"btn btn-action button-edit\" href=\"{{ .root }}markdown/view{{ .path }}\"><i class=\"icon icon-edit\">Code</i> Cancel</a>\n </div>\n <div class=\"form-group\">\n <textarea class=\"form-input\" id=\"input-body\" name=\"markdown\" placeholder=\"Content\" cols=\"80\" rows=\"24\">{{printf \"%s\" .raw}}</textarea>\n </div>\n </form>\n </div>\n <script src=\"{{ .root }}static/simplemde.min.js\"></script>\n <script>\n function editor() {\n var simplemde = new SimpleMDE({\n element: document.getElementById(\"input-body\"),\n autofocus: true,\n hideIcons: [\"side-by-side\", \"fullscreen\", \"guide\"],\n autosave: { enabled: false },\n spellChecker: false,\n forceSync: true,\n indentWithTabs: false,\n promptURLs: true,\n tabSize: 4,\n });\n };\n </script>\n</body>\n</html>"
|
||||
var _Assets52de3ee91e954b604e6683f45a4970540bf37631 = "{{ $root := .root }}<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"utf-8\">\n <title>Rizin Notebook</title>\n <link rel=\"stylesheet\" href=\"{{ $root }}static/spectre-icons.min.css\">\n <link rel=\"stylesheet\" href=\"{{ $root }}static/spectre.min.css\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <style type=\"text/css\">\n .spacing-top { margin: 3px 0px 0px 0px; }\n .spacing-left { margin: 0px 3px 0px 0px; }\n .align-center { text-align: center; }\n .pipe { color: #e85600; border-color: #e85600; }\n .pipe:hover { background: #e85600; border-color: #e85600; }\n .detail-size { width: 200px; }\n .background-white { background: white; }\n .spacer-12px { width: 12px; display: inline-block; }\n </style>\n</head>\n<body>\n <section class=\"container grid-960 mt-10\">\n <header class=\"navbar\">\n <section class=\"navbar-section\">\n <img src=\"{{ $root }}static/logo.png\" width=\"30\" height=\"30\" /> \n <a href=\"{{ $root }}\" class=\"navbar-brand mr-10\">Rizin Notebook</a>\n </section>\n <section class=\"navbar-section\">\n <a href=\"{{ $root }}\" class=\"btn btn-primary\">Go Back</a>\n </section>\n </header>\n <section class=\"container\">\n <p>\n \n <h3 class=\"s-title\"><i class=\"icon icon-edit\"></i><span class=\"spacer-12px\"></span>Rizin Notebook</h3>\n <div class=\"columns col-gapless\">\n <div class=\"column col-3 spacing-left\">\n <div class=\"input-group spacing-top\">\n <span class=\"input-group-addon detail-size\">Authors</span>\n </div>\n <div class=\"input-group spacing-top\">\n <span class=\"input-group-addon detail-size\">Version</span>\n </div>\n <div class=\"input-group spacing-top\">\n <span class=\"input-group-addon detail-size\">Storage</span>\n </div>\n </div>\n <div class=\"column\">\n <div class=\"input-group spacing-top\">\n <span class=\"input-group-addon\"><a href=\"https://twitter.com/der0ad\">@deroad</a></span>\n </div>\n <div class=\"input-group spacing-top\">\n <span class=\"input-group-addon\">{{ .nbversion }}</span>\n </div>\n <div class=\"input-group spacing-top\">\n <span class=\"input-group-addon\">{{ .storage }}</span>\n </div>\n </div>\n </div>\n </p>\n <p>\n <figure class=\"avatar avatar-lg background-white\">\n <img src=\"{{ $root }}static/logo.png\" alt=\"Rizin Logo\">\n </figure>\n <h3 class=\"s-title\" style=\"display: inline;\"> Rizin Framework</h3>\n <div class=\"columns col-gapless\">\n <div class=\"column col-3 spacing-left\">\n <div class=\"input-group spacing-top\">\n <span class=\"input-group-addon detail-size\">Version</span>\n </div>\n <div class=\"input-group spacing-top\">\n <span class=\"input-group-addon detail-size\">Build</span>\n </div>\n <div class=\"input-group spacing-top\">\n <span class=\"input-group-addon detail-size\">Location (RIZIN_PATH)</span>\n </div>\n </div>\n <div class=\"column\">\n <div class=\"input-group spacing-top\">\n <span class=\"input-group-addon\">{{ .rzversion }}</span>\n </div>\n <div class=\"input-group spacing-top\">\n <span class=\"input-group-addon\">{{ .rzbuild }}</span>\n </div>\n <div class=\"input-group spacing-top\">\n <span class=\"input-group-addon\">{{ .rzpath }}</span>\n </div>\n </div>\n </div>\n </p>\n </section>\n </section>\n</body>\n</html>"
|
||||
var _Assets6947bea9c2e7bfbff103ea8688234dfb7ab7d9d0 = "<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"utf-8\">\n <title>Rizin Notebook</title>\n <link rel=\"stylesheet\" href=\"{{ .root }}static/spectre-icons.min.css\">\n <link rel=\"stylesheet\" href=\"{{ .root }}static/spectre.min.css\">\n <link rel=\"stylesheet\" href=\"{{ .root }}static/simplemde.min.css\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <style type=\"text/css\">\n html, body { width: 100%; height: 100%; margin: 0 }\n .center { display: flex; justify-content: center; align-items: center; width: 100%; height: 100%; margin: 0 }\n .loading-xl::after { \n margin-left: -4.8rem;\n margin-top: -1.8rem;\n width: 3.6rem;\n height: 3.6rem;\n border: 0.3rem solid #5764c6;\n border-radius: 2.0rem;\n border-right-color:transparent;\n border-top-color:transparent;\n }\n </style>\n <meta http-equiv=\"refresh\" content=\"2\"/>\n</head>\n<body>\n <div class=\"center\">\n <div class=\"loading-xl loading\"></div> Waiting for rizin output. Please wait.\n </div>\n</body>\n</html>"
|
||||
var _Assets8d74849a5560d3b62ca6202780224621d98ab7c3 = "{{ $root := .root }}{{ $page := .page }}{{ $nlen := len .unique }}<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\">\n <title>Rizin Notebook</title>\n <link rel=\"stylesheet\" href=\"{{ $root }}static/spectre-icons.min.css\">\n <link rel=\"stylesheet\" href=\"{{ $root }}static/spectre.min.css\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <style type=\"text/css\">\n .spacing-top { margin: 3px 0px 0px 0px; }\n .spacing-left { margin: 0px 3px 0px 0px; }\n .navbar-fixed-height { height: 32px; }\n .icon-height { height: 20px; }\n iframe { border: 0px; width: 100%; height: 100% }\n </style>\n <script type=\"text/javascript\">\n function confirm_delete(event) {\n if (!window.confirm(\"Are you sure you want to delete it?\")) {\n event.preventDefault();\n }\n }\n function verify(event) {\n var title = document.getElementById('title').value.trim().length;\n var files = document.getElementById('binary').files.length;\n if (title < 1) {\n alert(\"Title field is empty. Please add one.\");\n event.preventDefault();\n } else if (files < 1) {\n alert(\"You forgot to select a file to analyze. Please select one.\");\n event.preventDefault();\n }\n }\n </script>\n </head>\n<body>\n <section class=\"container grid-960 mt-10\">\n <header class=\"navbar navbar-fixed-height\">\n <section class=\"navbar-section\">\n <img src=\"{{ $root }}static/logo.png\" width=\"30\" height=\"30\" /> \n <a href=\"{{ $root }}\" class=\"navbar-brand mr-10\">Rizin Notebook</a>\n </section>\n <section class=\"navbar-section\">\n {{ $page.title }}\n </section>\n </header>\n <section class=\"container\">\n <div class=\"columns\">\n <div class=\"column\">\n <form action=\"{{ $root }}edit\" method=\"POST\" enctype=\"multipart/form-data\" onsubmit=\"verify(event)\">\n <div class=\"form-group\">\n <input type=\"hidden\" name=\"unique\" value=\"{{ .unique }}\" />\n <input type=\"text\" class=\"form-input\" name=\"title\" id=\"title\" value=\"{{ .title }}\" placeholder=\"Notebook page title\"/>\n {{ if lt $nlen 1 }}\n <br>\n <label for=\"binary\">Choose a binary to open: </label>\n <input type=\"file\" id=\"binary\" name=\"binary\">\n {{ end }}\n </div>\n <div class=\"form-group\">\n <button class=\"btn btn-primary\" type=\"submit\">{{ if lt $nlen 1 }}Create{{ else }}Save{{ end }}</button>\n <a class=\"btn\" href=\"{{ $root }}\">Cancel</a>\n {{ if gt $nlen 0 }}\n <a class=\"btn float-right\" href=\"{{ $root }}delete/{{ .unique }}\" onclick=\"confirm_delete(event)\">Delete</a>\n {{ end }}\n </div>\n </form>\n </div>\n </div>\n </section>\n </section>\n</body>\n</html>"
|
||||
var _Assets8fdbadd31c8f0302966785b472dbe1cdec02f9b4 = "\x00\x01\x00\x00\x00\x12\x01\x00\x00\x04\x00 GDEFV\xb3T\x99\x00\x01Vx\x00\x00\x014GPOS\r\xda\x10\x8a\x00\x01W\xac\x00\x00\x1c\x92GSUB\x16\xdaN\xfc\x00\x01t@\x00\x00\t\xc4OS/2_\x95#1\x00\x01\f\x94\x00\x00\x00`STAT\xf5\xb7߮\x00\x01~\x04\x00\x00\x00Dcmap-e\xdcz\x00\x01\f\xf4\x00\x00\n@cvt \x16\xeb\r\xba\x00\x01)\x84\x00\x00\x00\xf0fpgm\xe2\x19\x9eZ\x00\x01\x174\x00\x00\x0f\x94gasp\x00\x00\x00\x10\x00\x01Vp\x00\x00\x00\bglyf@7.\x0f\x00\x00\x01,\x00\x00\xf4\\head\x19A\x8d\x10\x00\x00\xfd0\x00\x00\x006hhea\b\xbd\x06\xc6\x00\x01\fp\x00\x00\x00$hmtx\t\x14\xd3\xf6\x00\x00\xfdh\x00\x00\x0f\bloca+,\xee\xe5\x00\x00\xf5\xa8\x00\x00\a\x86maxp\az\x10\xd4\x00\x00\xf5\x88\x00\x00\x00 name\x93~\xb0\xdb\x00\x01*t\x00\x00\x05`post\xa2\x18\xb3\xb6\x00\x01/\xd4\x00\x00&\x9aprep+\xfar:\x00\x01&\xc8\x00\x00\x02\xbb\x00\x02\x00\x10\x00\x00\x01\xe4\x02z\x00\x03\x00\n\x00\x1b@\r\x04\x06\x02\x01\x01\x06\a\x03r\n\x06\br\x00+\xc4+\x119/3\x11901e#73'\x03#\x133\x13#\x01e\xe2\x0e\xc4\\\xa4E\xe6\b\xe6H\xb66\xee\xfe&\x02z\xfd\x86\x00\xff\xff\x00\x10\x00\x00\x01\xe4\x03\x17\x06&\x00\x01\x00\x00\x01\x06\x03\xa3\xed\x00\x00\v\xb6\x02\v\a\x01\x01\x1aV\x00+4\x00\xff\xff\x00\x10\x00\x00\x01\xe4\x03\x13\x06&\x00\x01\x00\x00\x01\x06\x03\xa7\x00\x00\x00\v\xb6\x02\x0f\a\x01\x01%V\x00+4\x00\xff\xff\x00\x10\x00\x00\x01\xe4\x03\x86\x06&\x00\x01\x00\x00\x00&\x03\xa7\x00\x00\x01\x06\x03\xa3\vo\x00\x15@\x0e\x03\x1a\x16\x01\x014V\x02\x0f\a\x01\x01%V\x00+4+4\x00\xff\xff\x00\x10\xffm\x01\xe4\x03\x13\x06&\x00\x01\x00\x00\x00&\x03\x8a\v\x00\x01\x06\x03\xa7\x00\x00\x00\x18@\f\x03\x1b\a\x01\x01%V\x02\x11\n\x01\x01\xb8\xff\u0530V\x00+4+4\xff\xff\x00\x10\x00\x00\x01\xe4\x03\xbb\x06&\x00\x01\x00\x00\x00&\x03\xa7\x00\x00\x01\a\x03\xa2\xff\xf2\x00\xa4\x00\x15@\x0e\x03\x1a\x16\x01\x01iV\x02\x0f\a\x01\x01%V\x00+4+4\x00\xff\xff\x00\x10\x00\x00\x01\xe4\x03\xb7\x06&\x00\x01\x00\x00\x00&\x03\xa7\x00\x00\x01\a\x03\x85\x00\x05\x00\xf7\x00\x15@\x0e\x03!\x16\x01\x013V\x02\x0f\a\x01\x01%V\x00+4+4\x00\xff\xff\x00\x10\x00\x00\x01\xe4\x03\xa2\x06&\x00\x01\x00\x00\x00&\x03\xa7\x00\x00\x01\a\x03\xb4\xff\xf1\x00\xf7\x00\x15@\x0e\x03\x1e\x16\x01\x01fV\x02\x0f\a\x01\x01%V\x00+4+4\x00\xff\xff\x00\x10\x00\x00\x01\xe4\x03\x17\x06&\x00\x01\x00\x00\x01\x06\x03\xa5\x00\x00\x00\v\xb6\x02\v\a\x01\x01\x15V\x00+4\x00\xff\xff\x00\x10\x00\x00\x02\x1a\x03\x81\x06&\x00\x01\x00\x00\x01\x06\x03\xbd\xff\x00\x00\x15@\x0e\x03\x13\v\x01\x01\x99V\x02\v\a\x01\x01\x15V\x00+4+4\x00\xff\xff\x00\x10\xffm\x01\xe4\x03\x17\x06&\x00\x01\x00\x00\x00&\x03\x8a\v\x00\x01\x06\x03\xa5\x00\x00\x00\x18@\f\x03\x17\a\x01\x01\x15V\x02\x11\n\x01\x01\xb8\xff\u0530V\x00+4+4\xff\xff\x00\x10\x00\x00\x01\xe4\x03\xa1\x06&\x00\x01\x00\x00\x01\x06\x03\xbe\x00\x00\x00\x15@\x0e\x03\x12\v\x01\x01\x8fV\x02\v\a\x01\x01\x15V\x00+4+4\x00\xff\xff\x00\x10\x00\x00\x01\xe4\x03\xa2\x06&\x00\x01\x00\x00\x01\x06\x03\xbf\xfc\x00\x00\x15@\x0e\x03\x19\v\x01\x01^V\x02\v\a\x01\x01\x15V\x00+4+4\x00\xff\xff\x00\x10\x00\x00\x01\xe4\x03\xa5\x06&\x00\x01\x00\x00\x01\x06\x03\xc0\x00\x00\x00\x15@\x0e\x03\x16\v\x01\x01\xa9V\x02\v\a\x01\x01\x15V\x00+4+4\x00\xff\xff\x00\x10\x00\x00\x01\xe4\x03P\x06&\x00\x01\x00\x00\x01\a\x03\x86\xff\xeb\x00\xa6\x00\r\xb7\x03\x02\v\a\x01\x01'V\x00+44\x00\xff\xff\x00\x10\x00\x00\x01\xe4\x03\x15\x06&\x00\x01\x00\x00\x01\x06\x03\xa0\xff\x00\x00\r\xb7\x03\x02\v\a\x01\x016V\x00+44\x00\xff\xff\x00\x10\xffm\x01\xe4\x02z\x06&\x00\x01\x00\x00\x01\x06\x03\x8a\v\x00\x00\x0e\xb4\x02\x11\n\x01\x01\xb8\xff\u0530V\x00+4\xff\xff\x00\x10\x00\x00\x01\xe4\x03\x17\x06&\x00\x01\x00\x00\x01\x06\x03\xa2\xf2\x00\x00\v\xb6\x02\v\a\x01\x01\x1aV\x00+4\x00\xff\xff\x00\x10\x00\x00\x01\xe4\x03f\x06&\x00\x01\x00\x00\x01\a\x03\x85\x00\x0f\x00\xa6\x00\v\xb6\x02\x12\a\x01\x017V\x00+4\x00\xff\xff\x00\x10\x00\x00\x01\xe4\x03*\x06&\x00\x01\x00\x00\x01\a\x03\x87\xff\xf8\x00\xa6\x00\v\xb6\x02\v\a\x01\x01<V\x00+4\x00\xff\xff\x00\x10\x00\x00\x01\xe4\x03\x13\x06&\x00\x01\x00\x00\x01\a\x03\x84\x00\x00\x00\xa6\x00\v\xb6\x02\v\a\x01\x01aV\x00+4\x00\xff\xff\x00\x10\xff\x1d\x01\xe4\x02z\x06&\x00\x01\x00\x00\x01\a\x03\x8e\x00\xa5\x00\x00\x00\v\xb6\x02\x15\n\x00\x00\x00V\x00+4\x00\xff\xff\x00\x10\x00\x00\x01\xe4\x03\x1b\x06&\x00\x01\x00\x00\x01\x06\x03\xa8\xff\x00\x00\x10\xb5\x03\x02\v\a\x00\x00\xb8\xff\xf1\xb0V\x00+44\xff\xff\x00\x10\x00\x00\x01\xe4\x03\xc3\x06&\x00\x01\x00\x00\x00&\x03\xa8\xff\x00\x01\a\x03\xa3\xff\xed\x00\xac\x00\x1a@\r\x04'\a\x01\x01\xc6V\x03\x02\v\a\x00\x00\xb8\xff\xf1\xb0V\x00+44+4\xff\xff\x00\x10\x00\x00\x01\xe4\x03\x1d\x06&\x00\x01\x00\x00\x01\x06\x03\xa9\xff\x00\x00\v\xb6\x02\x0f\a\x01\x016V\x00+4\x00\x00\x02\x00\x0f\x00\x00\x01\xef\x02p\x00\x0f\x00\x13\x00+@\x15\x10\x0e\x05\b\x0e\b\x0e\b\x00\x13\x04\x04\x01\x02r\t\f\f\x00\br\x00+2/3+2\x113\x1199//\x113\x11301s\x13!\x15#\x153\x15#\x153\x15#5#\a73\x11#\x0f\xc4\x01\x1c\xab\x91\x91\xa9\xe6\x826Dt\v\x02p>\xcd@\xe7>\xa9\xa9\xe3\x01O\x00\xff\xff\x00\x0f\x00\x00\x01\xef\x03\x17\x06&\x00\x1a\x00\x00\x01\x06\x03\xa3\x00\x00\x00\v\xb6\x02\x14\x01\x01\x01$V\x0Line truncated
|
||||
var _Assetscbf8cb58af475c51e110eb6ea4f06b938a70a87b = "/*! Spectre.css | MIT License | github.com/picturepan2/spectre */html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}article,aside,footer,header,nav,section{display:block}h1{font-size:2em;margin:.67em 0}figcaption,figure,main{display:block}hr{box-sizing:content-box;height:0;overflow:visible}a{background-color:transparent;-webkit-text-decoration-skip:objects}a:active,a:hover{outline-width:0}b,strong{font-weight:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}dfn{font-style:italic}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}audio,video{display:inline-block}audio:not([controls]){display:none;height:0}img{border-style:none}svg:not(:root){overflow:hidden}button,input,optgroup,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}fieldset{border:0;margin:0;padding:0}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{display:inline-block;vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details,menu{display:block}summary{display:list-item}canvas{display:inline-block}template{display:none}[hidden]{display:none}*,::after,::before{box-sizing:inherit}html{box-sizing:border-box;font-size:10px;line-height:1.42857143;-webkit-tap-highlight-color:transparent}body{background:#fff;color:#50596c;font-family:-apple-system,system-ui,BlinkMacSystemFont,\"Segoe UI\",Roboto,\"Helvetica Neue\",sans-serif;font-size:1.4rem;overflow-x:hidden;text-rendering:optimizeLegibility}a{color:#5764c6;outline:0;text-decoration:none}a:focus{box-shadow:0 0 0 .2rem rgba(87,100,198,.15)}a.active,a:active,a:focus,a:hover{color:#3b49af;text-decoration:underline}h1,h2,h3,h4,h5,h6{color:inherit;font-weight:300;line-height:1.2;margin-bottom:1.5rem;margin-top:0}h1{font-size:5rem}h2{font-size:4rem}h3{font-size:3rem}h4{font-size:2.4rem}h5{font-size:2rem}h6{font-size:1.6rem}p{line-height:2.4rem;margin:0 0 1rem}a,ins,u{-webkit-text-decoration-skip:ink edges;text-decoration-skip:ink edges}blockquote{border-left:.2rem solid #f0f1f4;margin-left:0;padding:1rem 2rem}blockquote p:last-child{margin-bottom:0}blockquote cite{color:#acb3c2}ol,ul{margin:2rem 0 2rem 2rem;padding:0}ol ol,ol ul,ul ol,ul ul{margin:1.5rem 0 1.5rem 2rem}ol li,ul li{margin-top:1rem}ul{list-style:disc inside}ul ul{list-style-type:circle}ol{list-style:decimal inside}ol ol{list-style-type:lower-alpha}dl dt{font-weight:700}dl dd{margin:.5rem 0 1.5rem 0}mark{background:#ffe9b3;border-radius:.2rem;color:#50596c;display:inline-block;line-height:1;padding:.3rem .4rem;vertical-align:baseline}kbd{background:#454d5d;border-radius:.2rem;color:#fff;display:inline-block;line-height:1;padding:.3rem .4rem;vertical-align:baseline}abbr[title]{border-bottom:.1rem dotted;cursor:help;text-decoration:none}.cjk,:lang(ja),:lang(ko),:lang(zh){font-family:-apple-system,system-ui,BlinkMacSystemFont,\"Segoe UI\",Roboto,\"PingFang SC\",\"Hiragino Sans GB\",\"Microsoft YaHei\",\"Hiragino Kaku Gothic Pro\",Meiryo,\"Malgun Gothic\",\"Helvetica Neue\",sans-serif}.cjk ins,.cjk u,:lang(ja) ins,:lang(ja) u,:lang(zh) ins,:lang(zh) u{border-bottom:.1rem solid;text-decoration:none}.cjk del+del,.cjk del+s,.cjk ins+ins,.cjk ins+u,.cjk s+del,.cjk s+s,.cjk u+ins,.cjk u+u,:lang(ja) del+del,:lang(ja) del+s,:lang(ja) ins+ins,:lang(ja) ins+u,:lang(ja) s+del,:lang(ja) s+s,:lang(ja) u+ins,:lang(ja) u+u,:lang(zh) del+del,:lang(zh) del+s,:lang(zh) ins+ins,:lang(zh) ins+u,:lang(zh) s+del,:lang(zh) s+s,:lang(zh) u+ins,:lang(zh) u+u{margin-left:.125em}.table{border-collapse:collapse;border-spacing:0;text-align:left;width:100%}.table.table-striped tbody tr:nth-of-type(odd){background:#f8f9fa}.table.table-hover tbody tr:hover{background:#f0f1f4}.table tbody tr.active,.table.table-striped tbody tr.active{background:#f0f1f4}.table td{border-bottom:.1rem solid #f0f1f4;padding:1.5rem 1rem}.table th{border-bottom:.1rem solid #727e96;padding:1.5rem 1rem}.btn{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:#fff;border:.1rem solid #5764c6;border-radius:.2rem;color:#5764c6;cursor:pointer;display:inline-block;font-size:1.4rem;Line truncated
|
||||
var _Assetsf87fad8398be841cc0ef4de3b92ce630d9cb8549 = "/**\n * simplemde v1.11.2\n * Copyright Next Step Webs, Inc.\n * @link https://github.com/NextStepWebs/simplemde-markdown-editor\n * @license MIT\n */\n.CodeMirror{color:#000}.CodeMirror-lines{padding:4px 0}.CodeMirror pre{padding:0 4px}.CodeMirror-gutter-filler,.CodeMirror-scrollbar-filler{background-color:#fff}.CodeMirror-gutters{border-right:1px solid #ddd;background-color:#f7f7f7;white-space:nowrap}.CodeMirror-linenumber{padding:0 3px 0 5px;min-width:20px;text-align:right;color:#999;white-space:nowrap}.CodeMirror-guttermarker{color:#000}.CodeMirror-guttermarker-subtle{color:#999}.CodeMirror-cursor{border-left:1px solid #000;border-right:none;width:0}.CodeMirror div.CodeMirror-secondarycursor{border-left:1px solid silver}.cm-fat-cursor .CodeMirror-cursor{width:auto;border:0!important;background:#7e7}.cm-fat-cursor div.CodeMirror-cursors{z-index:1}.cm-animate-fat-cursor{width:auto;border:0;-webkit-animation:blink 1.06s steps(1) infinite;-moz-animation:blink 1.06s steps(1) infinite;animation:blink 1.06s steps(1) infinite;background-color:#7e7}@-moz-keyframes blink{50%{background-color:transparent}}@-webkit-keyframes blink{50%{background-color:transparent}}@keyframes blink{50%{background-color:transparent}}.cm-tab{display:inline-block;text-decoration:inherit}.CodeMirror-ruler{border-left:1px solid #ccc;position:absolute}.cm-s-default .cm-header{color:#00f}.cm-s-default .cm-quote{color:#090}.cm-negative{color:#d44}.cm-positive{color:#292}.cm-header,.cm-strong{font-weight:700}.cm-em{font-style:italic}.cm-link{text-decoration:underline}.cm-strikethrough{text-decoration:line-through}.cm-s-default .cm-keyword{color:#708}.cm-s-default .cm-atom{color:#219}.cm-s-default .cm-number{color:#164}.cm-s-default .cm-def{color:#00f}.cm-s-default .cm-variable-2{color:#05a}.cm-s-default .cm-variable-3{color:#085}.cm-s-default .cm-comment{color:#a50}.cm-s-default .cm-string{color:#a11}.cm-s-default .cm-string-2{color:#f50}.cm-s-default .cm-meta,.cm-s-default .cm-qualifier{color:#555}.cm-s-default .cm-builtin{color:#30a}.cm-s-default .cm-bracket{color:#997}.cm-s-default .cm-tag{color:#170}.cm-s-default .cm-attribute{color:#00c}.cm-s-default .cm-hr{color:#999}.cm-s-default .cm-link{color:#00c}.cm-invalidchar,.cm-s-default .cm-error{color:red}.CodeMirror-composing{border-bottom:2px solid}div.CodeMirror span.CodeMirror-matchingbracket{color:#0f0}div.CodeMirror span.CodeMirror-nonmatchingbracket{color:#f22}.CodeMirror-matchingtag{background:rgba(255,150,0,.3)}.CodeMirror-activeline-background{background:#e8f2ff}.CodeMirror{position:relative;overflow:hidden;background:#fff}.CodeMirror-scroll{overflow:scroll!important;margin-bottom:-30px;margin-right:-30px;padding-bottom:30px;height:100%;outline:0;position:relative}.CodeMirror-sizer{position:relative;border-right:30px solid transparent}.CodeMirror-gutter-filler,.CodeMirror-hscrollbar,.CodeMirror-scrollbar-filler,.CodeMirror-vscrollbar{position:absolute;z-index:6;display:none}.CodeMirror-vscrollbar{right:0;top:0;overflow-x:hidden;overflow-y:scroll}.CodeMirror-hscrollbar{bottom:0;left:0;overflow-y:hidden;overflow-x:scroll}.CodeMirror-scrollbar-filler{right:0;bottom:0}.CodeMirror-gutter-filler{left:0;bottom:0}.CodeMirror-gutters{position:absolute;left:0;top:0;min-height:100%;z-index:3}.CodeMirror-gutter{white-space:normal;height:100%;display:inline-block;vertical-align:top;margin-bottom:-30px}.CodeMirror-gutter-wrapper{position:absolute;z-index:4;background:0 0!important;border:none!important;-webkit-user-select:none;-moz-user-select:none;user-select:none}.CodeMirror-gutter-background{position:absolute;top:0;bottom:0;z-index:4}.CodeMirror-gutter-elt{position:absolute;cursor:default;z-index:4}.CodeMirror-lines{cursor:text;min-height:1px}.CodeMirror pre{-moz-border-radius:0;-webkit-border-radius:0;border-radius:0;border-width:0;background:0 0;font-family:inherit;font-size:inherit;margin:0;white-space:pre;word-wrap:normal;line-height:inherit;color:inherit;z-index:2;position:relative;overflow:visible;-webkit-tap-highlight-color:transparent;-webkit-font-variant-ligatures:none;font-variant-ligatures:none}.CodeMirror-wrap pre{word-wrap:break-word;white-space:pre-wrap;word-break:normal}.CodeMirror-linebackground{position:absolute;left:0;right:0;top:0;bottom:0;z-index:0}.CodeMirror-linewidget{position:relative;z-index:2;overflow:auto}.CodeMirror-code{outline:0}.CodeMirror-gutter,.CodeMirror-gutters,.CodeMirror-linenumber,.CodeMirror-scroll,.CodeMirror-sizer{-moz-box-sizing:content-box;box-sizing:content-box}.CodeMirror-measure{position:absolute;width:100%;height:0;overflow:hidden;visibility:hidden}.CodeMirror-cursor{position:absolute}.CodeMirror-measure pre{position:static}div.CodeMirror-cursors{visibility:hidden;position:relative;z-index:3}.CodeMirror-focused div.CodeMirror-cursors,div.CodeMirror-dragcursors{visibility:visible}.CodeMirror-selected{background:#d9d9d9}.CodeMirror-focused .CodeMirror-selected,.CodeMirror-line::selection,.CodLine truncated
|
||||
var _Assetsaad1e392c9fe33a13d63fefdb59493ea12de3f06 = "\x00\x00\x01\x00\x01\x00\x10\x10\x00\x00\x01\x00 \x00h\x04\x00\x00\x16\x00\x00\x00(\x00\x00\x00\x10\x00\x00\x00 \x00\x00\x00\x01\x00 \x00\x00\x00\x00\x00\x00\x04\x00\x00\x12\v\x00\x00\x12\v\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\xff\xff\xff\xfc\xfe\xff\xff\xff\xff\xff\xff\xd2\xef\xfc\xffo\xcc\xf5\xffF\xbd\xf3\xffA\xbb\xf2\xffC\xbc\xf2\xffF\xbd\xf3\xffm\xcb\xf5\xff\xd2\xef\xfc\xff\xff\xff\xff\xff\xfc\xfe\xff\xff\xfe\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff\xfe\xff\xff\xff\xfd\xfe\xff\xff~\xd1\xf6\xffE\xbd\xf2\xff\x96\xda\xf8\xff\xd5\xf0\xfc\xff\xf6\xfc\xfe\xff\xec\xf8\xfe\xff\xd9\xf1\xfc\xff\x9d\xdc\xf8\xffD\xbc\xf2\xff~\xd1\xf6\xff\xfd\xfe\xff\xff\xfe\xff\xff\xff\xfe\xff\xff\xff\xfe\xff\xff\xff\xff\xff\xff\xffX\xc3\xf4\xffk\xca\xf5\xff\xfb\xfe\xff\xff\xff\xff\xff\xff\xfe\xfe\xff\xff\xe6\xf6\xfd\xff\xff\xff\xff\xff\xf8\xfd\xfe\xff\xf2\xfa\xfe\xff\xfd\xfe\xff\xffi\xca\xf5\xffX\xc4\xf4\xff\xff\xff\xff\xff\xfe\xff\xff\xff\xff\xff\xff\xffy\xcf\xf6\xffn\xcb\xf5\xff\xff\xff\xff\xff\xff\xff\xff\xff\xd9\xf1\xfc\xffW\xc3\xf4\xffM\xc0\xf3\xff\xbc\xe7\xfa\xff1\xb6\xf2\xff\x8a\xd5\xf7\xff\xff\xff\xff\xff\xff\xff\xff\xffo\xcc\xf5\xffy\xcf\xf6\xff\xff\xff\xff\xff\xcb\xec\xfb\xffN\xc0\xf3\xff\xcf\xee\xfb\xff\x93\xd8\xf7\xff\xff\xff\xff\xff\xb4\xe4\xf9\xff\x19\xad\xf0\xff>\xba\xf3\xff\x81\xd2\xf6\xff\x02\xa5\xef\xff\x84\xd3\xf7\xff\xff\xff\xff\xff\xaf\xe2\xf9\xff\xcf\xee\xfb\xffN\xc0\xf3\xff\xcb\xec\xfb\xfff\xc9\xf4\xff\xa8\xe0\xf9\xffr\xcd\xf6\xfff\xc8\xf5\xff\xff\xff\xfe\xff\xff\xff\xff\xff\xd7\xf0\xfc\xff\xa4\xde\xf8\xff\xff\xff\xff\xff\xb6\xe5\xf9\xff\xc2\xe9\xfa\xff\xff\xff\xff\xff~\xd1\xf6\xffr\xcd\xf5\xff\xa7\xe0\xf9\xffg\xc9\xf5\xffH\xbe\xf3\xff\xcd\xed\xfb\xff+\xb4\xf1\xff\\\xc5\xf4\xff\xf3\xfa\xfd\xff\xdd\xf3\xfc\xff\xe9\xf7\xfd\xff\xf1\xf9\xfd\xff\xe2\xf4\xfc\xff\xef\xf9\xfd\xff\xe8\xf6\xfd\xff\xf4\xfa\xfd\xffu\xce\xf6\xff)\xb3\xf1\xff\xcd\xed\xfb\xffH\xbe\xf3\xffL\xbf\xf3\xff\xc9\xec\xfb\xff\x1f\xb0\xf1\xff+\xb4\xf1\xff\xd0\xee\xfb\xff\xec\xf8\xfd\xff\xe3\xf5\xfd\xff\xe2\xf4\xfd\xff\xe6\xf6\xfd\xff\xe3\xf5\xfd\xff\xe7\xf6\xfd\xff\xe1\xf4\xfc\xff:\xb9\xf2\xff\x1c\xaf\xf1\xff\xcb\xec\xfb\xffM\xc0\xf3\xffL\xbf\xf3\xff\xc9\xec\xfb\xff \xb0\xf1\xff\x03\xa6\xef\xffJ\xbf\xf3\xff\xec\xf8\xfd\xff\xff\xff\xff\xff\xf6\xfc\xfe\xff\xf4\xfb\xfe\xff\xff\xff\xff\xff\xf6\xfb\xfe\xffb\xc7\xf5\xff\x00\xa5\xef\xff\x1e\xaf\xf1\xff\xcb\xec\xfb\xffM\xbf\xf3\xffI\xbe\xf3\xff\xc8\xeb\xfb\xffG\xbd\xf3\xff\x89\xd5\xf7\xff\xc3\xe9\xfb\xff\xfd\xfe\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff\xff\xff\xff\xff\xcb\xec\xfc\xff\x91\xd8\xf8\xffQ\xc1\xf3\xff\xc5\xea\xfb\xffI\xbe\xf3\xffc\xc8\xf4\xff\xa8\xe0\xf9\xff{\xd0\xf6\xff\x9a\xdb\xf8\xff\xff\xff\xff\xff\xf5\xfb\xfe\xff\xde\xf3\xfc\xff\xfc\xfe\xff\xff\xff\xff\xff\xff\xdf\xf3\xfc\xff\xf2\xfa\xfe\xff\xff\xff\xff\xff\xb0\xe3\xf9\xff\x84\xd3\xf6\xff\xa6\xdf\xf9\xffd\xc8\xf5\xff\xc7\xeb\xfb\xffA\xbb\xf2\xff\xc5\xea\xfa\xff\xd6\xf0\xfc\xff\xf5\xfb\xfe\xff\xd8\xf1\xfc\xff\xdc\xf2\xfc\xff\xf7\xfc\xfe\xff\xfb\xfe\xff\xff\xdd\xf3\xfc\xff\xd8\xf0\xfc\xff\xf0\xfa\xfe\xff\xde\xf3\xfd\xff\xc7\xea\xfa\xff<\xb9\xf2\xff\xc9\xeb\xfb\xff\xff\xff\xff\xffr\xcd\xf5\xffu\xce\xf5\xff\xf6\xfc\xfe\xff\xbe\xe7\xfa\xff\xf0\xf9\xfd\xff\xe9\xf7\xfd\xff\xe7\xf6\xfd\xff\xe7\xf6\xfd\xff\xe7\xf6\xfd\xff\xed\xf8\xfd\xff\xca\xec\xfb\xff\xf5\xfb\xfe\xffr\xcd\xf5\xffr\xcd\xf5\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfb\xfe\xff\xffQ\xc1\xf3\xffx\xcf\xf6\xff\xd4\xef\xfb\xff\xea\xf8\xfd\xff\xad\xe1\xf9\xff\xf6\xfc\xfe\xff\xfb\xfd\xfe\xff\xb4\xe4\xf9\xff\xe3\xf5\xfd\xff\xd5\xf0\xfb\xffu\xce\xf6\xffR\xc1\xf3\xff\xfb\xfe\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff\xff\xff\xff\xff\xf8\xfc\xfe\xffv\xce\xf6\xff9\xb9\xf1\xff\x83\xd3\xf6\xff\xc7\xeb\xfb\xff\xdb\xf2\xfc\xff\xdc\xf2\xfc\xff\xc8\xeb\xfb\xff\x86\xd4\xf6\xff3\xb6\xf1\xffw\xce\xf6\xff\xf8\xfc\xfe\xff\xff\xff\xff\xff\xfe\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff\xfc\xfe\xff\xff\xff\xff\xff\xff\xc9\xec\xfb\xffi\xc9\xf5\xffK\xbf\xf3\xff;\xb9\xf2\xff8\xb8\xf2\xffK\xbf\xf3\xffi\xc9\xf5\xff\xcb\xec\xfb\xff\xff\xff\xff\xff\xfd\xfe\xff\xff\xfe\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
var _Assets25ed14e5f6bcc6e42a5e6dee7c116155e2480e3b = "/**\n * simplemde v1.11.2\n * Copyright Next Step Webs, Inc.\n * @link https://github.com/NextStepWebs/simplemde-markdown-editor\n * @license MIT\n */\n!function(e){if(\"object\"==typeof exports&&\"undefined\"!=typeof module)module.exports=e();else if(\"function\"==typeof define&&define.amd)define([],e);else{var t;t=\"undefined\"!=typeof window?window:\"undefined\"!=typeof global?global:\"undefined\"!=typeof self?self:this,t.SimpleMDE=e()}}(function(){var e;return function t(e,n,r){function i(a,l){if(!n[a]){if(!e[a]){var s=\"function\"==typeof require&&require;if(!l&&s)return s(a,!0);if(o)return o(a,!0);var c=new Error(\"Cannot find module '\"+a+\"'\");throw c.code=\"MODULE_NOT_FOUND\",c}var u=n[a]={exports:{}};e[a][0].call(u.exports,function(t){var n=e[a][1][t];return i(n?n:t)},u,u.exports,t,e,n,r)}return n[a].exports}for(var o=\"function\"==typeof require&&require,a=0;a<r.length;a++)i(r[a]);return i}({1:[function(e,t,n){\"use strict\";function r(){for(var e=\"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/\",t=0,n=e.length;n>t;++t)s[t]=e[t],c[e.charCodeAt(t)]=t;c[\"-\".charCodeAt(0)]=62,c[\"_\".charCodeAt(0)]=63}function i(e){var t,n,r,i,o,a,l=e.length;if(l%4>0)throw new Error(\"Invalid string. Length must be a multiple of 4\");o=\"=\"===e[l-2]?2:\"=\"===e[l-1]?1:0,a=new u(3*l/4-o),r=o>0?l-4:l;var s=0;for(t=0,n=0;r>t;t+=4,n+=3)i=c[e.charCodeAt(t)]<<18|c[e.charCodeAt(t+1)]<<12|c[e.charCodeAt(t+2)]<<6|c[e.charCodeAt(t+3)],a[s++]=i>>16&255,a[s++]=i>>8&255,a[s++]=255&i;return 2===o?(i=c[e.charCodeAt(t)]<<2|c[e.charCodeAt(t+1)]>>4,a[s++]=255&i):1===o&&(i=c[e.charCodeAt(t)]<<10|c[e.charCodeAt(t+1)]<<4|c[e.charCodeAt(t+2)]>>2,a[s++]=i>>8&255,a[s++]=255&i),a}function o(e){return s[e>>18&63]+s[e>>12&63]+s[e>>6&63]+s[63&e]}function a(e,t,n){for(var r,i=[],a=t;n>a;a+=3)r=(e[a]<<16)+(e[a+1]<<8)+e[a+2],i.push(o(r));return i.join(\"\")}function l(e){for(var t,n=e.length,r=n%3,i=\"\",o=[],l=16383,c=0,u=n-r;u>c;c+=l)o.push(a(e,c,c+l>u?u:c+l));return 1===r?(t=e[n-1],i+=s[t>>2],i+=s[t<<4&63],i+=\"==\"):2===r&&(t=(e[n-2]<<8)+e[n-1],i+=s[t>>10],i+=s[t>>4&63],i+=s[t<<2&63],i+=\"=\"),o.push(i),o.join(\"\")}n.toByteArray=i,n.fromByteArray=l;var s=[],c=[],u=\"undefined\"!=typeof Uint8Array?Uint8Array:Array;r()},{}],2:[function(e,t,n){},{}],3:[function(e,t,n){(function(t){\"use strict\";function r(){try{var e=new Uint8Array(1);return e.foo=function(){return 42},42===e.foo()&&\"function\"==typeof e.subarray&&0===e.subarray(1,1).byteLength}catch(t){return!1}}function i(){return a.TYPED_ARRAY_SUPPORT?2147483647:1073741823}function o(e,t){if(i()<t)throw new RangeError(\"Invalid typed array length\");return a.TYPED_ARRAY_SUPPORT?(e=new Uint8Array(t),e.__proto__=a.prototype):(null===e&&(e=new a(t)),e.length=t),e}function a(e,t,n){if(!(a.TYPED_ARRAY_SUPPORT||this instanceof a))return new a(e,t,n);if(\"number\"==typeof e){if(\"string\"==typeof t)throw new Error(\"If encoding is specified then the first argument must be a string\");return u(this,e)}return l(this,e,t,n)}function l(e,t,n,r){if(\"number\"==typeof t)throw new TypeError('\"value\" argument must not be a number');return\"undefined\"!=typeof ArrayBuffer&&t instanceof ArrayBuffer?d(e,t,n,r):\"string\"==typeof t?f(e,t,n):p(e,t)}function s(e){if(\"number\"!=typeof e)throw new TypeError('\"size\" argument must be a number')}function c(e,t,n,r){return s(t),0>=t?o(e,t):void 0!==n?\"string\"==typeof r?o(e,t).fill(n,r):o(e,t).fill(n):o(e,t)}function u(e,t){if(s(t),e=o(e,0>t?0:0|m(t)),!a.TYPED_ARRAY_SUPPORT)for(var n=0;t>n;n++)e[n]=0;return e}function f(e,t,n){if(\"string\"==typeof n&&\"\"!==n||(n=\"utf8\"),!a.isEncoding(n))throw new TypeError('\"encoding\" must be a valid string encoding');var r=0|v(t,n);return e=o(e,r),e.write(t,n),e}function h(e,t){var n=0|m(t.length);e=o(e,n);for(var r=0;n>r;r+=1)e[r]=255&t[r];return e}function d(e,t,n,r){if(t.byteLength,0>n||t.byteLength<n)throw new RangeError(\"'offset' is out of bounds\");if(t.byteLength<n+(r||0))throw new RangeError(\"'length' is out of bounds\");return t=void 0===r?new Uint8Array(t,n):new Uint8Array(t,n,r),a.TYPED_ARRAY_SUPPORT?(e=t,e.__proto__=a.prototype):e=h(e,t),e}function p(e,t){if(a.isBuffer(t)){var n=0|m(t.length);return e=o(e,n),0===e.length?e:(t.copy(e,0,0,n),e)}if(t){if(\"undefined\"!=typeof ArrayBuffer&&t.buffer instanceof ArrayBuffer||\"length\"in t)return\"number\"!=typeof t.length||K(t.length)?o(e,0):h(e,t);if(\"Buffer\"===t.type&&J(t.data))return h(e,t.data)}throw new TypeError(\"First argument must be a string, Buffer, ArrayBuffer, Array, or array-like object.\")}function m(e){if(e>=i())throw new RangeError(\"Attempt to allocate Buffer larger than maximum size: 0x\"+i().toString(16)+\" bytes\");return 0|e}function g(e){return+e!=e&&(e=0),a.alloc(+e)}function v(e,t){if(a.isBuffer(e))return e.length;if(\"undefined\"!=typeof ArrayBuffer&&\"function\"==typeof ArrayBuffer.isView&&(ArrayBuffer.isView(e)||e instanceof ArrayBuffer))return e.byteLengLine truncated
|
||||
var _Assetsebfcd4e56920358b26e898adc7fbb3022afd8a3e = "{{ $root := .root }}<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"utf-8\">\n <title>Rizin Notebook</title>\n <link rel=\"stylesheet\" href=\"{{ $root }}static/spectre-icons.min.css\">\n <link rel=\"stylesheet\" href=\"{{ $root }}static/spectre.min.css\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <style type=\"text/css\">\n .spacing-top { margin: 3px 0px 0px 0px; }\n .spacing-left { margin: 0px 3px 0px 0px; }\n .height-32px { height: 32px; }\n .align-center { text-align: center; }\n .pipe { color: #e85600; border-color: #e85600; }\n .pipe:hover { background: #e85600; border-color: #e85600; }\n .detail-size { width: 200px; }\n .background-white { background: white; }\n .spacer-12px { width: 12px; display: inline-block; }\n .col-btn { width: 3.333%; }\n </style>\n <script type=\"text/javascript\">\n function keycombo(elem, keys) {\n keys = keys.split(',');\n if (keys.length === 1 && keys[0].length < 1) {\n keys = [];\n }\n elem.textContent = keys.length > 0 ? keys.join(' + ') : \"Not Assigned\";\n }\n </script>\n</head>\n<body>\n <section class=\"container grid-960 mt-10\">\n <header class=\"navbar\">\n <section class=\"navbar-section\">\n <img src=\"{{ $root }}static/logo.png\" width=\"30\" height=\"30\" /> \n <a href=\"{{ $root }}\" class=\"navbar-brand mr-10\">Rizin Notebook</a>\n </section>\n <section class=\"navbar-section\">\n <a href=\"{{ $root }}\" class=\"btn btn-primary\">Go Back</a>\n </section>\n </header>\n <section class=\"container\">\n <p>\n \n <h3 class=\"s-title\">Environment Variables</h3>\n <div class=\"columns col-gapless\">\n <div class=\"column col-1 col-btn spacing-left\">\n{{ range $envkey, $env := .environment }}\n <a class=\"btn spacing-top\" href=\"{{ $root }}settings/environment/edit/{{ $envkey }}\">\n <i class=\"icon icon-edit\">Edit</i>\n </a>\n{{ end }}\n </div>\n <div class=\"column col-3 spacing-left\">\n{{ range $envkey, $env := .environment }}\n <div class=\"input-group\">\n <span class=\"input-group-addon detail-size spacing-top height-32px\">{{ $envkey }}</span>\n </div>\n{{ end }}\n </div>\n <div class=\"column col-8\">\n{{ range $envkey, $env := .environment }}\n <div class=\"input-group spacing-left\">\n <span class=\"input-group-addon height-32px spacing-top\">{{ $env }}</span>\n </div>\n{{ end }}\n </div>\n </div>\n <a class=\"btn\" href=\"{{ $root }}settings/environment/edit/new\">\n New Environment Variable\n </a>\n </p>\n </section>\n </section>\n</body>\n</html>"
|
||||
var _Assetse3765268b98f9d73e1fee1dfddb9c8e4001d6f7a = "<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"utf-8\">\n <title>Rizin Notebook</title>\n <link rel=\"stylesheet\" href=\"{{ .root }}static/spectre-icons.min.css\">\n <link rel=\"stylesheet\" href=\"{{ .root }}static/spectre.min.css\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <style type=\"text/css\">\n html, body { width: 100%; height: 100%; margin: 0 }\n .button-edit { float: right; width: 60px ! important; margin-top: 5px; }\n .markdown-container { margin-top: 9px; }\n </style>\n</head>\n<body onload=\"view()\">\n <a class=\"btn btn-action spacing-left button-edit\" href=\"{{ .root }}markdown/edit{{ .path }}\">\n <i class=\"icon icon-edit\">Code</i> Edit\n </a>\n <div class=\"markdown-container\" id=\"markdown-container\" >\n <textarea style=\"display: none;\" name=\"md-container\" id=\"md-container\" cols=\"30\" rows=\"10\">{{ .html }}</textarea>\n </div>\n <script src=\"{{ .root }}static/simplemde.min.js\"></script>\n <script>\n function view() {\n var smde = new SimpleMDE({\n element: document.getElementById('md-container'),\n autoDownloadFontAwesome: false,\n spellChecker: false,\n status: false,\n indentWithTabs: false,\n promptURLs: true,\n tabSize: 4,\n });\n var md = smde.value();\n document.getElementById('markdown-container').innerHTML = smde.markdown(md);\n };\n </script>\n</body>\n</html>"
|
||||
var _Assetsb45a00d76b7dcbaa3b9c30ab2b2d16404478b419 = "{{ $error := .error }}{{ $llen := len .location }}<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"utf-8\">\n <title>Rizin Notebook</title>\n <link rel=\"stylesheet\" href=\"{{ .root }}static/spectre-icons.min.css\">\n <link rel=\"stylesheet\" href=\"{{ .root }}static/spectre.min.css\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <style type=\"text/css\">\n .spacing-top { margin: 3px 0px 0px 0px; }\n .spacing-left { margin: 0px 3px 0px 0px; }\n .align-center { text-align: center; }\n </style>\n</head>\n<body>\n <section class=\"container grid-960 mt-10\">\n <header class=\"navbar\">\n <section class=\"navbar-section\">\n <img src=\"{{ .root }}static/logo.png\" width=\"30\" height=\"30\" /> \n <a href=\"{{ .root }}\" class=\"navbar-brand mr-10\">Rizin Notebook</a>\n </section>\n <section class=\"navbar-section\">\n <a href=\"{{ if lt $llen 1 }}{{ .root }}{{ else }}{{ .location }}{{ end }}\" class=\"btn btn-primary\">Go Back</a>\n </section>\n </header>\n <section class=\"container\">\n <div class=\"columns\">\n <div class=\"column\">\n <div class=\"input-group spacing-top\">\n <span class=\"input-group-addon align-center spacing-left\">{{ $error }}</span>\n </div>\n </div>\n </div>\n </section>\n </section>\n</body>\n</html>"
|
||||
var _Assetsab269208dc370b8abed2a667e16e03ca0567fade = "\x00\x00\x01\x00\x01\x00\x10\x10\x00\x00\x01\x00 \x00h\x04\x00\x00\x16\x00\x00\x00(\x00\x00\x00\x10\x00\x00\x00 \x00\x00\x00\x01\x00 \x00\x00\x00\x00\x00\x00\x04\x00\x00\xc3\x0e\x00\x00\xc3\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00===!??? DDD\x1eAAA\x1f>>> ===!===!===!===!===!===!===!===!===!===!===!??? ))).\x16\x16\x16K 9...*===!===!===!===!===!===!===!===!===!===!===!DDD\x1e\x16\x16\x16K\x01\x01\x01\xe4\x01\x01\x01\xe2\t\t\t\x81>>>#@@@!===!===!===!===!===!===!===!===!===!AAA\x1f 8\x01\x01\x01\xe0\x05\x05\x05\xc2\x1a\x1a\x1ad\n\n\n\xa5\x14\x14\x14iAAA ===!===!===!===!===!===!===!===!>>> ...*\n\n\n\x81\x1a\x1a\x1ae\b\b\b\xc6\x00\x00\x00\xf1\x02\x02\x02\xe6\x11\x11\x11dBBB\x1f===!===!===!===!===!===!===!===!===!>>>%\v\v\v\xa6\x00\x00\x00\xed\x03\x03\x03U\x04\x04\x04\xa8\x02\x02\x02\xe8\x11\x11\x11dBBB\x1f===!===!===!===!===!===!===!===!@@@\"\x15\x15\x15k\x02\x02\x02\xe5\x04\x04\x04\xa7\x1f\x1f\x1f?\x06\x06\x06\xad\x02\x02\x02\xe7\x11\x11\x11dBBB\x1f===!===!===!===!===!===!===!===!AAA \x11\x11\x11d\x02\x02\x02\xe8\x06\x06\x06\xad\x1f\x1f\x1f?\x06\x06\x06\xad\x02\x02\x02\xe8\x11\x11\x11dBBB\x1f===!===!===!===!===!===!===!===!BBB\x1f\x11\x11\x11d\x02\x02\x02\xe8\x06\x06\x06\xad\x1f\x1f\x1f?\x06\x06\x06\xad\x02\x02\x02\xe8\x11\x11\x11dBBB\x1f===!===!===!===!===!===!===!===!BBB\x1f\x11\x11\x11d\x02\x02\x02\xe7\x06\x06\x06\xad\x1f\x1f\x1f?\x06\x06\x06\xad\x02\x02\x02\xe6\x11\x11\x11dBBB\x1f===!===!===!===!===!===!===!===!BBB\x1f\x11\x11\x11d\x02\x02\x02\xe8\x06\x06\x06\xad\x1d\x1d\x1dB\x04\x04\x04\xc5\x02\x02\x02\xeb\x11\x11\x11dAAA\x1f===!===!===!===!===!===!===!===!BBB\x1f\x11\x11\x11d\x02\x02\x02\xe6\x04\x04\x04\xc6\x02\x02\x02\xdc\x02\x02\x02\xdc\x03\x03\x03\xe3\x11\x11\x11b@@@ ===!===!===!===!===!===!===!===!BBB\x1f\x11\x11\x11d\x02\x02\x02\xea\x03\x03\x03\xd9\x0f\x0f\x0fn\x02\x02\x02\xed\x06\x06\x06\xad999#===!===!===!===!===!===!===!===!===!BBB\x1f\x11\x11\x11d\x03\x03\x03\xe3\x02\x02\x02\xef\x04\x04\x04\xd3\x19\x19\x19L@@@\x1f===!===!===!===!===!===!===!===!===!===!AAA\x1f\x11\x11\x11b\x05\x05\x05\xaf\x19\x19\x19LCCC\x1e===!===!===!===!===!===!===!===!===!===!===!===!@@@ 999#@@@\x1f===!===!\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
var _Assetsbfdf4a3e904014e3d8fb363c3d1206b6f5180288 = "<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"utf-8\">\n <title>Rizin Notebook</title>\n <link rel=\"stylesheet\" href=\"{{ .root }}static/spectre-icons.min.css\">\n <link rel=\"stylesheet\" href=\"{{ .root }}static/spectre.min.css\">\n <link rel=\"stylesheet\" href=\"{{ .root }}static/simplemde.min.css\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <style type=\"text/css\">\n html, body { width: 100%; height: 100%; margin: 0 }\n .button-edit { width: 75px ! important; margin-top: 9px; margin-right: 9px; }\n </style>\n</head>\n<body>\n <form style=\"margin-top: 15px; height: 100%;\" action=\"{{ .root }}output/exec/{{ .unique }}\" method=\"POST\">\n <div class=\"form-group\">\n <input type=\"text\" class=\"form-input\" name=\"command\" value=\"\" placeholder=\"Rizin command for example: pdf\"/>\n </div>\n <div class=\"form-group\">\n <button class=\"btn btn-primary\" type=\"submit\">Exec</button>\n <a class=\"btn\" href=\"{{ .root }}output/deleted\">Cancel</a>\n </div>\n </form>\n</body>\n</html>"
|
||||
var _Assets217fba07788194e5eb162b7794a0b9d7622d092c = "<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"utf-8\">\n <title>Rizin Notebook</title>\n <link rel=\"stylesheet\" href=\"{{ .root }}static/spectre-icons.min.css\">\n <link rel=\"stylesheet\" href=\"{{ .root }}static/spectre.min.css\">\n <link rel=\"stylesheet\" href=\"{{ .root }}static/simplemde.min.css\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <style type=\"text/css\">\n blink { -webkit-animation: 2s linear infinite condemned_blink_effect; animation: 2s linear infinite condemned_blink_effect; }\n @-webkit-keyframes condemned_blink_effect {\n 0% { visibility: hidden; }\n 50% { visibility: hidden; }\n 100% { visibility: visible; }\n }\n @keyframes condemned_blink_effect {\n 0% { visibility: hidden; }\n 50% { visibility: hidden; }\n 100% { visibility: visible; }\n }\n @font-face { font-family: 'Inconsolata'; src: URL('{{ .root }}static/inconsolata.ttf') format('truetype'); }\n html, body { width: 100%; height: 100%; background-color: #4b4b4b; color: #eeeeee; font-family: Inconsolata; }\n pre { width: 100%; height: 100%; margin: 5px; background-color: inherit; color: inherit; line-height: 8px; }\n </style>\n</head>\n<body>\n <pre>{{ .output | raw }}</pre>\n</body>\n</html>"
|
||||
var _Assetse134fa82eca7c17da76f3698c373a24c1feacbdd = "\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x01\xa5\x00\x00\x01\xa4\b\x06\x00\x00\x00\x91\x8e:\v\x00\x00\x01\x84iCCPICC profile\x00\x00(\x91}\x91=H\xc3P\x14\x85OS\xb5\"\x15\a;\x888d\xa8N\x16DE\x1cm\x15\x8aP!\xd4\n\xad:\x98\xbc\xf4\x0f\x9a4$).\x8e\x82k\xc1\xc1\x9fŪ\x83\x8b\xb3\xae\x0e\xae\x82 \xf8\x03\xe2\xe4\xe8\xa4\xe8\"%ޗ\x14Z\xc4\xf8\xe0\xf2>\xce{\xe7p\xdf}\x80Ш0\xcd\xea\x9a\x004\xdd6\xd3Ʉ\x98ͭ\x8a\xa1W\x84!PE\xd1#3ˈKR\n\xbe\xeb\xeb\x1e\x01\xbe\xdf\xc5x\x96\xff\xbd?W\xbf\x9a\xb7\x18\x10\x10\x89\xe7\x98a\xda\xc4\x1b\xc43\x9b\xb6\xc1y\x9f8\xc2J\xb2J|N<nR\x83ď\\W<~\xe3\\tY\xe0\x99\x113\x93\x9e'\x8e\x10\x8b\xc5\x0eV:\x98\x95L\x8dx\x9a8\xaaj:\xe5\vY\x8fU\xce[\x9c\xb5J\x8d\xb5\xfa\xe4/\f\xe7\xf5\x95e\xaeS\x8d \x89E,A\x82\b\x055\x94Q\x81\x8d\x18\xed:)\x16\xd2t\x9e\xf0\xf1\x0f\xbb~\x89\\\n\xb9\xca`\xe4X@\x15\x1ad\xd7\x0f\xfe\a\xbfgk\x15\xa6&\xbd\xa4p\x02\xe8~q\x9c\x8fQ \xb4\v4\xeb\x8e\xf3}\xec8\xcd\x13 \xf8\f\\\xe9m\u007f\xb5\x01\xcc~\x92^ok\xd1#``\x1b\xb8\xb8nk\xca\x1ep\xb9\x03\f=\x19\xb2)\xbbR\x90J(\x14\x80\xf73\xfa\xa6\x1c0x\v\xf4\xadysk\x9d\xe3\xf4\x01\xc8ЬR7\xc0\xc1!0V\xa4\xecu\x9fw\xf7v\xce\xed\xdf;\xad\xf9\xfd\x00\x147r\x81tP\xfen\x00\x00\x00\x06bKGD\x00\xff\x00\xff\x00\xff\xa0\xbd\xa7\x93\x00\x00\x00\tpHYs\x00\x00.#\x00\x00.#\x01x\xa5?v\x00\x00\x00\atIME\a\xe4\f\x02\f&;U7E\xda\x00\x00\x00\x19tEXtComment\x00Created with GIMPW\x81\x0e\x17\x00\x00 \x00IDATx\xda\xec\xbdi\xb8]\xd5q&\xfcV\xad\xb5\x87sν\xbaW\x12\x92\x90\x84\x04\x92\x11\x11\x18\x8f\xe0\xe0`\u007f\x8013\b!\x90\xb1\x13\x9b\xb8\xe3ٴ\u06ddď;Iw\x92ǝ\xa4\xbf8\xed/\x9d\xc4I\xbb\xdbI\x9c\x84\xc4N\x1c\x1b3\x89\x19\x1b\xc7\xc4C\xb0\xe3ı\xb1!\x16 @\x13\x12\xba\xd2\x1dΰ\x87\xb5\xaa\xbe\x1fk\x9f\xe1^\x04\x06\x04h\xe0\xd4\xf3\xe89\xbaҹg\xef\xb3\xf7\xda\xf5\xae\xaaz\xeb-\xc2І\xf6\x12\xb5\x9d_\xfbPͻ\xe6Y\xed\xd6\xe4\x1b\x98\x8a\xc5\xd0b1\xa1\\\xc0pc\x04\xdf`H\x02\x00\x11%;\xa1\xe4H\xc8y\xf8\x06\ty!IH\xc8\tIj`ZB\x92\x18\x98\x96\xb2ZVΔ\xd5\x1a\x98\x16H\x12\xe7\xdc\x18\x00\xa8\xaa\x05\x00\x01g \xd3\"2-\x10\xe7\x85\xe3\x9cM\xbc\xdb\xc6#\xf7\xc5\xf1\xc8}\x8b\xcf\xf9\xf3o\f\xef\xce\xd0^\xaaF\xc3K0\xb4#ն\xdcy\xf5\xf1\xae3\xf5\xfaT\xf3\xa5\xac\xf92\x117F,\x89aI\f\xb91\xd5|\x19i\xbe\x94P\xce\a\xe7 80J0\t@\x0e\xac\x02\x80\xa1.\x02\x02\x9e\x80\x88f\xbd\x0e\x9a\xaa\xee\xe7U`\r\x01\x90\xfe\xfb\xaa_U0\x00\x86W\x86\xc2«\x05Ԋ\xa8\x9d\"2-\x85\x9d.)\x9a\xc8M\xfd\xc1\x12\xf1nA4\x01\xaem9钿\xb9vxw\x876\x04\xa5\xa1\r\xedP\x8cv\xee\xbcjc\xd1\xdes\x11Sg\x15\xa1\\@T,f\x95\x04\xe4\x1b\x00@\n\x18圴\xfb\x1b\x92\xb0\x02 \x89I\x05 \r\xe0C\x0e\x14\xfe\x1dT\x81\nWP\xa2D\x15\x80<ׇL\x00\x04\x80\x02\t\x14\xe18\xe1\xdf\x00!\x02*`\x02\xa8\x02\xc0\x00XP\x03\x92\xe4q\xa8\x9dR\x92T(\x80\x9a'\xc0\x93\x9d\x12\x98\x96\x83\x9d\x12J\xb7*\xd77\xdbd\xec\xde\xe3\xce\xfd\xcc=Õ1\xb4!(\rmh/\x82m\xfe\xc2U\x1b\xadN\x9fjm\xf3d˭\xb5\x06\xad\x97Y\xce@\xc8\xc1\xf0\xc1\xd9+\xf7\x97\xb72\x00\xdb\x0fO\x9eb\xd1\xf7A\xab\vG\x00+ \x14\xa0CI@\xca\xcfᵂBB\x05J\x158A\xc0\x10H\xef\xc0\xdd\xf0\x89\a\x00\x89`\x84\x11{\vR\x81\xb2@\xc8C\xb9\x80\x90B(\x00\x9aP\x04\x0f\v/1D\xd2}\x1e\xb5-D\x8d\a\x98\x92\x1d\x85I\xb7\xeeS\xce_\xbb\xee\xf3\x9f\x1e\xae\x9e\xa1\rAihC{\x0e6\xf5\x8f\xffqQ\xd6\xda\xf9\x81\"\x9f<\xdd\x17\x9dU\xf5\xa8\xb6\x05\xe2\xc6D\xcb\x05\x80\x1b\xa3H\x12b\xdfP\x141i\t\x82\ai\x00\x11\x02\xc0B e\x90r\x15\x91\bt6\xea<\xad\r\xe0W\x05VREP\xcf\x01\x94\xc0\xf0\x88\xa0Ľϩ\xe2\xaf*\x82\x1a\x80BE\x0f\x8c\xba\xc0H\n\x18x\x00\x0e\xa0.\x88I?\x05H\xa8\xd2\u007f\fB\x04A\x04\x15\vQ\x03\xf1$\xaa\xe4\xd24ݪJ\xae\x10\xb5\x85\a\x1cŻM:vomt\xd95K\xce\xf8\xf4\xf7\x86+nhCP\x1a\xda\xd0\xe6\xd8\xf6[\xde}\x06\x15\xfb΄\xdf{\xb6\xd1\xe9S,wF#.@$\xd0*\xf2Q\x00j\xaaWUx\xf8\xb0\x88\xc9TN=\x80\x11+\x03\xca\xd5\xe2\x16\x80\xf2\n\f\xc2\xefҜ\xd7\x10\xc9p/2\x9amR\x81\x9d<\xa7\xef\xa5\xe0\x00\x14\xc4\x03@'\xb3B3\xd6وH\xbdH\x8f\x01r\x00|\xf5:\xf0\xc6Y\x11!@\x14\xa2+\x82\tǪP\x8b\x15\xd0,\x875\x06l\t\xc2\x06\x85*\n\x8a\u0a31Gx\xf4{\x99\x8e\xdcW\xd2ؽ?u\xc9g??\\\x89C\x1b\x82\xd2\xd0^z\x00t\xfb{O\xcb:\xbb7Dh\xad\x8dm\xb9\xc0\xe5ӧZr5&\x0f\xcb\x04\x86\u0080\xa0\x1a\xa2\t6\x02U\x81\xaaB\xd5\x03\xaa \"\x10\f\xc0\x04\x91.\xe0P\x0f`\xfaю\x87A\x1f\x94\x04\x00+C(\x80\x98\f\x90\x0eH\xbb\xbe\x9cg\x81R\xb7\xd6\xf4\\\x8d\x15\xb3\x00d\xe0\u007f*\x90\x9a\x9dn$\xf4߯$P\xf6\xc0\x93@Q\x06\"\xc1\x80q\xaa\xd4#Y\x00\x80\xa9\xae\x87\xe5\b$\n\x0f@T\xe1A\x00\x11\x14\x16b\fr\uf85c\x160\xe9VQ;Ux;M\x9c>62o\xfe=\x8b\xce\xfe\xbb\xcf\fW\xecІ\xa04\xb4#\xce\xf6\xdc\xf4\xf3\x1b\x8a|\xea4ՙWŶ\xb3\xca\xd8\xce*\xf5\xed\xd8\xfb\x16\xea\xa9\x05D\xe0U!\x1eP\r\xbb}f\v\"\x82/3\x18\x02\x98\x19\xdce\xbf\x89\xf6\x9d0s/\x955X\xc3\t\x84\x02\a\x82\x0f\xf5\x1b\xcc\x05\x9c\xc1:\xcel\xa0\x98\v\x00\xcf\xfd\t\x13\xb0j\x15\x18\xd1\xecc\fFDO\xf38jU?\xda_dE\n\x88\b\x88\xb4\u007fm\xa8\xcf\xf6#\"\x88\x84c\x11\x11\x88\b\xcc\fU\x85\xa8\x83\xaa\x82-\xc19\a/\x02\x98\x18L\x11D\t\xde\x132I\x8b2]v\x8d3G\xddv\xc2E\u007f}\xfdp%\x0fm\bJC;,\xed\xb1ۯ\xfcp\xd1\u07b5\xd1 [\x19\x93\x9bg\xd87\f\xa9%\x94LR\x80\xe0\x111\xc1\xb2A\x9e\xb5\xc1\xcLine truncated
|
||||
var _Assets463d1a70a0b97ff5743af8f39e2c5bb780f845bd = "{{ $root := .root }}{{ $eklen := len .editkey }}<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\">\n <title>Rizin Notebook</title>\n <link rel=\"stylesheet\" href=\"{{ $root }}static/spectre-icons.min.css\">\n <link rel=\"stylesheet\" href=\"{{ $root }}static/spectre.min.css\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <style type=\"text/css\">\n .spacing-top { margin: 3px 0px 0px 0px; }\n .spacing-left { margin: 0px 3px 0px 0px; }\n .navbar-fixed-height { height: 32px; }\n .inline { display: inline; }\n .icon-height { height: 20px; }\n iframe { border: 0px; width: 100%; height: 100% }\n </style>\n <script type=\"text/javascript\">\n{{ if eq .action \"environment\" }}\n function confirm_delete(event) {\n if (!window.confirm(\"Are you sure you want to delete it?\")) {\n event.preventDefault();\n }\n document.getElementById('subaction').value = \"delete\";\n }\n function verify(event) {\n var key = document.getElementById('key').value.trim().length;\n if (key < 1) {\n alert(\"Environment Variable Name field is empty.\");\n event.preventDefault();\n }\n }\n{{ end }}\n </script>\n </head>\n<body>\n <section class=\"container grid-960 mt-10\">\n <header class=\"navbar navbar-fixed-height\">\n <section class=\"navbar-section\">\n <img src=\"{{ $root }}static/logo.png\" width=\"30\" height=\"30\" /> \n <a href=\"{{ $root }}\" class=\"navbar-brand mr-10\">Rizin Notebook</a>\n </section>\n <section class=\"navbar-section\">\n Edit Settings\n </section>\n </header>\n <section class=\"container\">\n <div class=\"columns\">\n <div class=\"column\">\n <form action=\"{{ $root }}settings\" method=\"POST\" enctype=\"multipart/form-data\" onsubmit=\"verify(event)\">\n <div class=\"form-group\">\n <input type=\"hidden\" name=\"action\" value=\"{{ .action }}\" />\n {{ if eq .action \"environment\" }}\n <input type=\"hidden\" name=\"editkey\" value=\"{{ .editkey }}\" />\n <input type=\"hidden\" name=\"subaction\" id=\"subaction\" value=\"{{ if eq $eklen 0 }}new{{ else }}edit{{ end }}\" />\n <label class=\"form-label\" for=\"key\">Environment Variable Name</label>\n <input type=\"text\" class=\"form-input\" name=\"key\" id=\"key\" value=\"{{ .editkey }}\" placeholder=\"Environment Variable Name\"/>\n <label class=\"form-label\" for=\"value\">Value</label>\n <input type=\"text\" class=\"form-input\" name=\"value\" id=\"value\" value=\"{{ if gt $eklen 0 }}{{ index .data .editkey }}{{ end }}\" placeholder=\"Insert a variable value (or leave it empty)\"/>\n {{ end }}\n </div>\n <div class=\"form-group\">\n <button class=\"btn btn-primary\" type=\"submit\">{{ if lt $eklen 1 }}Create{{ else }}Save{{ end }}</button>\n {{ if eq .action \"environment\" }}\n {{ if gt $eklen 0 }}\n <a class=\"btn\" href=\"{{ $root }}settings\">Cancel</a>\n <button class=\"btn float-right\" type=\"submit\" onclick=\"confirm_delete(event)\">Delete</button>\n {{ else }}\n <a class=\"btn\" href=\"{{ $root }}settings\">Cancel</a>\n {{ end }}\n {{ else }}\n <button class=\"btn float-right\" type=\"submit\">Cancel</button>\n {{ end }}\n </div>\n </form>\n </div>\n </div>\n </section>\n </section>\n</body>\n</html>"
|
||||
var _Assets21dac258b9b707af506e61e4ebf21f784f182cc2 = "/*! Spectre.css Icons | MIT License | github.com/picturepan2/spectre */.icon{box-sizing:border-box;display:inline-block;font-size:inherit;font-style:normal;height:1em;position:relative;text-indent:-999rem;vertical-align:middle;width:1em}.icon::after,.icon::before{display:block;left:50%;position:absolute;top:50%;-webkit-transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%);transform:translate(-50%,-50%)}.icon-arrow-down::before,.icon-arrow-left::before,.icon-arrow-right::before,.icon-arrow-up::before,.icon-back::before,.icon-downward::before,.icon-forward::before,.icon-upward::before{border:.2rem solid currentColor;border-bottom:0;border-right:0;content:\"\";height:.65em;-webkit-transform:translate(-25%,-50%) rotate(-45deg);-ms-transform:translate(-25%,-50%) rotate(-45deg);transform:translate(-25%,-50%) rotate(-45deg);width:.65em}.icon-arrow-down,.icon-downward{-webkit-transform:rotate(-90deg);-ms-transform:rotate(-90deg);transform:rotate(-90deg)}.icon-arrow-right,.icon-forward{-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.icon-arrow-up,.icon-upward{-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.icon-back::before,.icon-downward::before,.icon-forward::before,.icon-upward::before{-webkit-transform:translate(-50%,-50%) rotate(-45deg);-ms-transform:translate(-50%,-50%) rotate(-45deg);transform:translate(-50%,-50%) rotate(-45deg)}.icon-back::after,.icon-downward::after,.icon-forward::after,.icon-upward::after{background:currentColor;content:\"\";height:.2rem;left:55%;width:.8em}.icon-caret::before{border-left:.3em solid transparent;border-right:.3em solid transparent;border-top:.3em solid currentColor;content:\"\";height:0;-webkit-transform:translate(-50%,-25%);-ms-transform:translate(-50%,-25%);transform:translate(-50%,-25%);width:0}.icon-menu::before{background:currentColor;box-shadow:0 -.35em,0 .35em;content:\"\";height:.2rem;width:100%}.icon-apps::before{background:currentColor;box-shadow:-.35em -.35em,-.35em 0,-.35em .35em,0 -.35em,0 .35em,.35em -.35em,.35em 0,.35em .35em;content:\"\";height:.3rem;width:.3rem}.icon-resize-horiz::after,.icon-resize-horiz::before,.icon-resize-vert::after,.icon-resize-vert::before{border:.2rem solid currentColor;border-bottom:0;border-right:0;content:\"\";height:.45em;width:.45em}.icon-resize-horiz::before,.icon-resize-vert::before{-webkit-transform:translate(-50%,-90%) rotate(45deg);-ms-transform:translate(-50%,-90%) rotate(45deg);transform:translate(-50%,-90%) rotate(45deg)}.icon-resize-horiz::after,.icon-resize-vert::after{-webkit-transform:translate(-50%,-10%) rotate(225deg);-ms-transform:translate(-50%,-10%) rotate(225deg);transform:translate(-50%,-10%) rotate(225deg)}.icon-resize-horiz::before{-webkit-transform:translate(-90%,-50%) rotate(-45deg);-ms-transform:translate(-90%,-50%) rotate(-45deg);transform:translate(-90%,-50%) rotate(-45deg)}.icon-resize-horiz::after{-webkit-transform:translate(-10%,-50%) rotate(135deg);-ms-transform:translate(-10%,-50%) rotate(135deg);transform:translate(-10%,-50%) rotate(135deg)}.icon-more-horiz::before,.icon-more-vert::before{background:currentColor;border-radius:50%;box-shadow:-.4em 0,.4em 0;content:\"\";height:.3rem;width:.3rem}.icon-more-vert::before{box-shadow:0 -.4em,0 .4em}.icon-cross::before,.icon-minus::before,.icon-plus::before{background:currentColor;content:\"\";height:.2rem;width:100%}.icon-cross::after,.icon-plus::after{background:currentColor;content:\"\";height:100%;width:.2rem}.icon-cross::before{width:100%}.icon-cross::after{height:100%}.icon-cross::after,.icon-cross::before{-webkit-transform:translate(-50%,-50%) rotate(45deg);-ms-transform:translate(-50%,-50%) rotate(45deg);transform:translate(-50%,-50%) rotate(45deg)}.icon-check::before{border:.2rem solid currentColor;border-right:0;border-top:0;content:\"\";height:.5em;-webkit-transform:translate(-50%,-75%) rotate(-45deg);-ms-transform:translate(-50%,-75%) rotate(-45deg);transform:translate(-50%,-75%) rotate(-45deg);width:.9em}.icon-stop{border:.2rem solid currentColor;border-radius:50%}.icon-stop::before{background:currentColor;content:\"\";height:.2rem;-webkit-transform:translate(-50%,-50%) rotate(45deg);-ms-transform:translate(-50%,-50%) rotate(45deg);transform:translate(-50%,-50%) rotate(45deg);width:1em}.icon-shutdown{border:.2rem solid currentColor;border-radius:50%;border-top-color:transparent}.icon-shutdown::before{background:currentColor;content:\"\";height:.5em;top:.1em;width:.2rem}.icon-refresh::before{border:.2rem solid currentColor;border-radius:50%;border-right-color:transparent;content:\"\";height:1em;width:1em}.icon-refresh::after{border:.2em solid currentColor;border-left-color:transparent;border-top-color:transparent;content:\"\";height:0;left:80%;top:20%;width:0}.icon-search::before{border:.2rem solid currentColor;border-radius:50%;content:\"\";height:.75em;left:5%;top:5%;-webkit-transform:translate(0,0Line truncated
|
||||
|
||||
// Assets returns go-assets FileSystem
|
||||
var Assets = assets.NewFileSystem(map[string][]string{"/": []string{"assets"}, "/assets": []string{}, "/assets/static": []string{"jquery.slim.min.js", "favicon.ico", "inconsolata.ttf", "spectre.min.css", "simplemde.min.css", "spectre-icons.min.css", "rizin-favicon.ico", "logo.png", "simplemde.min.js"}, "/assets/templates": []string{"index.tmpl", "console-error.tmpl", "markdown-edit.tmpl", "settings.tmpl", "console.tmpl", "about.tmpl", "settings-edit.tmpl", "reload.tmpl", "page-new.tmpl", "output.tmpl", "markdown-view.tmpl", "error.tmpl", "page-view.tmpl"}}, map[string]*assets.File{
|
||||
"/assets/templates/about.tmpl": &assets.File{
|
||||
Path: "/assets/templates/about.tmpl",
|
||||
FileMode: 0x1a4,
|
||||
Mtime: time.Unix(1637028298, 1637028298465516838),
|
||||
Data: []byte(_Assets52de3ee91e954b604e6683f45a4970540bf37631),
|
||||
}, "/assets/templates/reload.tmpl": &assets.File{
|
||||
Path: "/assets/templates/reload.tmpl",
|
||||
FileMode: 0x1a4,
|
||||
Mtime: time.Unix(1637028298, 1637028298465516838),
|
||||
Data: []byte(_Assets6947bea9c2e7bfbff103ea8688234dfb7ab7d9d0),
|
||||
}, "/assets/templates/page-new.tmpl": &assets.File{
|
||||
Path: "/assets/templates/page-new.tmpl",
|
||||
FileMode: 0x1a4,
|
||||
Mtime: time.Unix(1637028298, 1637028298465516838),
|
||||
Data: []byte(_Assets8d74849a5560d3b62ca6202780224621d98ab7c3),
|
||||
}, "/assets/templates/page-view.tmpl": &assets.File{
|
||||
Path: "/assets/templates/page-view.tmpl",
|
||||
FileMode: 0x1a4,
|
||||
Mtime: time.Unix(1637028298, 1637028298465516838),
|
||||
Data: []byte(_Assetsccaa111ee439fc8a890b718441ba429bfa47c126),
|
||||
}, "/assets/static/jquery.slim.min.js": &assets.File{
|
||||
Path: "/assets/static/jquery.slim.min.js",
|
||||
FileMode: 0x1a4,
|
||||
Mtime: time.Unix(1634634682, 1634634682813164659),
|
||||
Data: []byte(_Assetsce3dea9f2eb66eda07e7b51153e713391edd7f4c),
|
||||
}, "/assets/templates/index.tmpl": &assets.File{
|
||||
Path: "/assets/templates/index.tmpl",
|
||||
FileMode: 0x1a4,
|
||||
Mtime: time.Unix(1637028298, 1637028298465516838),
|
||||
Data: []byte(_Assetsa73f174e8addbc8adb067ef8cce595d0ae511369),
|
||||
}, "/assets/templates/console-error.tmpl": &assets.File{
|
||||
Path: "/assets/templates/console-error.tmpl",
|
||||
FileMode: 0x1a4,
|
||||
Mtime: time.Unix(1637028298, 1637028298465516838),
|
||||
Data: []byte(_Assets92ed65f24ecf383f92d30c1d5413460a473ae582),
|
||||
}, "/assets/templates/markdown-edit.tmpl": &assets.File{
|
||||
Path: "/assets/templates/markdown-edit.tmpl",
|
||||
FileMode: 0x1a4,
|
||||
Mtime: time.Unix(1637028298, 1637028298465516838),
|
||||
Data: []byte(_Assets25265b46fc5005cf89577b3590eb083ef336b559),
|
||||
}, "/assets/static/rizin-favicon.ico": &assets.File{
|
||||
Path: "/assets/static/rizin-favicon.ico",
|
||||
FileMode: 0x1a4,
|
||||
Mtime: time.Unix(1611354557, 1611354557771466726),
|
||||
Data: []byte(_Assetsaad1e392c9fe33a13d63fefdb59493ea12de3f06),
|
||||
}, "/assets/static/simplemde.min.js": &assets.File{
|
||||
Path: "/assets/static/simplemde.min.js",
|
||||
FileMode: 0x1a4,
|
||||
Mtime: time.Unix(1611354557, 1611354557771466726),
|
||||
Data: []byte(_Assets25ed14e5f6bcc6e42a5e6dee7c116155e2480e3b),
|
||||
}, "/assets/static/inconsolata.ttf": &assets.File{
|
||||
Path: "/assets/static/inconsolata.ttf",
|
||||
FileMode: 0x1a4,
|
||||
Mtime: time.Unix(1637028298, 1637028298462183401),
|
||||
Data: []byte(_Assets8fdbadd31c8f0302966785b472dbe1cdec02f9b4),
|
||||
}, "/assets/static/spectre.min.css": &assets.File{
|
||||
Path: "/assets/static/spectre.min.css",
|
||||
FileMode: 0x1a4,
|
||||
Mtime: time.Unix(1611354557, 1611354557771466726),
|
||||
Data: []byte(_Assetscbf8cb58af475c51e110eb6ea4f06b938a70a87b),
|
||||
}, "/assets/static/simplemde.min.css": &assets.File{
|
||||
Path: "/assets/static/simplemde.min.css",
|
||||
FileMode: 0x1a4,
|
||||
Mtime: time.Unix(1611354557, 1611354557771466726),
|
||||
Data: []byte(_Assetsf87fad8398be841cc0ef4de3b92ce630d9cb8549),
|
||||
}, "/assets/static/favicon.ico": &assets.File{
|
||||
Path: "/assets/static/favicon.ico",
|
||||
FileMode: 0x1a4,
|
||||
Mtime: time.Unix(1611354557, 1611354557768133350),
|
||||
Data: []byte(_Assetsab269208dc370b8abed2a667e16e03ca0567fade),
|
||||
}, "/assets/templates/settings.tmpl": &assets.File{
|
||||
Path: "/assets/templates/settings.tmpl",
|
||||
FileMode: 0x1a4,
|
||||
Mtime: time.Unix(1637028298, 1637028298465516838),
|
||||
Data: []byte(_Assetsebfcd4e56920358b26e898adc7fbb3022afd8a3e),
|
||||
}, "/assets/templates/markdown-view.tmpl": &assets.File{
|
||||
Path: "/assets/templates/markdown-view.tmpl",
|
||||
FileMode: 0x1a4,
|
||||
Mtime: time.Unix(1637028298, 1637028298465516838),
|
||||
Data: []byte(_Assetse3765268b98f9d73e1fee1dfddb9c8e4001d6f7a),
|
||||
}, "/assets/templates/error.tmpl": &assets.File{
|
||||
Path: "/assets/templates/error.tmpl",
|
||||
FileMode: 0x1a4,
|
||||
Mtime: time.Unix(1637028298, 1637028298465516838),
|
||||
Data: []byte(_Assetsb45a00d76b7dcbaa3b9c30ab2b2d16404478b419),
|
||||
}, "/assets/static/logo.png": &assets.File{
|
||||
Path: "/assets/static/logo.png",
|
||||
FileMode: 0x1a4,
|
||||
Mtime: time.Unix(1611354557, 1611354557771466726),
|
||||
Data: []byte(_Assetse134fa82eca7c17da76f3698c373a24c1feacbdd),
|
||||
}, "/": &assets.File{
|
||||
Path: "/",
|
||||
FileMode: 0x800001ed,
|
||||
Mtime: time.Unix(1637028399, 1637028399995309805),
|
||||
Data: nil,
|
||||
}, "/assets/templates/console.tmpl": &assets.File{
|
||||
Path: "/assets/templates/console.tmpl",
|
||||
FileMode: 0x1a4,
|
||||
Mtime: time.Unix(1637028298, 1637028298465516838),
|
||||
Data: []byte(_Assetsbfdf4a3e904014e3d8fb363c3d1206b6f5180288),
|
||||
}, "/assets/templates/output.tmpl": &assets.File{
|
||||
Path: "/assets/templates/output.tmpl",
|
||||
FileMode: 0x1a4,
|
||||
Mtime: time.Unix(1637028298, 1637028298465516838),
|
||||
Data: []byte(_Assets217fba07788194e5eb162b7794a0b9d7622d092c),
|
||||
}, "/assets/static": &assets.File{
|
||||
Path: "/assets/static",
|
||||
FileMode: 0x800001ed,
|
||||
Mtime: time.Unix(1637028298, 1637028298462183401),
|
||||
Data: nil,
|
||||
}, "/assets/static/spectre-icons.min.css": &assets.File{
|
||||
Path: "/assets/static/spectre-icons.min.css",
|
||||
FileMode: 0x1a4,
|
||||
Mtime: time.Unix(1611354557, 1611354557771466726),
|
||||
Data: []byte(_Assets21dac258b9b707af506e61e4ebf21f784f182cc2),
|
||||
}, "/assets": &assets.File{
|
||||
Path: "/assets",
|
||||
FileMode: 0x800001ed,
|
||||
Mtime: time.Unix(1611354557, 1611354557771466726),
|
||||
Data: nil,
|
||||
}, "/assets/templates": &assets.File{
|
||||
Path: "/assets/templates",
|
||||
FileMode: 0x800001ed,
|
||||
Mtime: time.Unix(1637028298, 1637028298465516838),
|
||||
Data: nil,
|
||||
}, "/assets/templates/settings-edit.tmpl": &assets.File{
|
||||
Path: "/assets/templates/settings-edit.tmpl",
|
||||
FileMode: 0x1a4,
|
||||
Mtime: time.Unix(1637028456, 1637028456433696910),
|
||||
Data: []byte(_Assets463d1a70a0b97ff5743af8f39e2c5bb780f845bd),
|
||||
}}, "")
|
||||
@@ -0,0 +1,298 @@
|
||||
/* autocomplete.js — Rizin Notebook command autocomplete
|
||||
*
|
||||
* Provides inline autocomplete for rizin command input fields.
|
||||
* Fetches command data lazily from the server API and caches it.
|
||||
*
|
||||
* API:
|
||||
* NotebookAutocomplete.attach(inputElement, options)
|
||||
* options.apiUrl — URL to fetch commands (default: inferred from page root)
|
||||
* options.maxResults — maximum suggestions shown (default: 12)
|
||||
* options.minChars — minimum chars before suggesting (default: 2)
|
||||
*/
|
||||
(function (global, document) {
|
||||
"use strict";
|
||||
|
||||
/* ── Command cache ──────────────────────────────────── */
|
||||
|
||||
var _cache = null;
|
||||
var _fetching = null;
|
||||
|
||||
/**
|
||||
* Fetch commands from the server API. Returns a sorted array of
|
||||
* { name: string, cmd: RizinCommand } objects. Cached after first call.
|
||||
*/
|
||||
function fetchCommands(apiUrl) {
|
||||
if (_cache) return Promise.resolve(_cache);
|
||||
if (_fetching) return _fetching;
|
||||
_fetching = fetch(apiUrl)
|
||||
.then(function (r) {
|
||||
return r.json();
|
||||
})
|
||||
.then(function (data) {
|
||||
var list = [];
|
||||
for (var key in data) {
|
||||
if (data.hasOwnProperty(key)) {
|
||||
list.push({ name: key, cmd: data[key] });
|
||||
}
|
||||
}
|
||||
list.sort(function (a, b) {
|
||||
return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
|
||||
});
|
||||
_cache = list;
|
||||
_fetching = null;
|
||||
return list;
|
||||
})
|
||||
.catch(function () {
|
||||
_fetching = null;
|
||||
return [];
|
||||
});
|
||||
return _fetching;
|
||||
}
|
||||
|
||||
/* ── Attach autocomplete to an input ────────────────── */
|
||||
|
||||
function attach(input, options) {
|
||||
var opts = options || {};
|
||||
var apiUrl = opts.apiUrl || (global.NB_ROOT || "/") + "api/v1/commands";
|
||||
var maxResults = opts.maxResults || 12;
|
||||
var minChars = opts.minChars || 2;
|
||||
|
||||
var commands = null;
|
||||
var selIdx = -1;
|
||||
var items = [];
|
||||
var isOpen = false;
|
||||
|
||||
// Wrap input so dropdown can position relatively
|
||||
var wrap = input.parentNode;
|
||||
if (!wrap.classList.contains("nb-ac-wrap")) {
|
||||
var newWrap = document.createElement("div");
|
||||
newWrap.className = "nb-ac-wrap";
|
||||
newWrap.style.width = "100%";
|
||||
input.parentNode.insertBefore(newWrap, input);
|
||||
newWrap.appendChild(input);
|
||||
wrap = newWrap;
|
||||
}
|
||||
|
||||
// Create dropdown
|
||||
var dropdown = document.createElement("div");
|
||||
dropdown.className = "nb-ac-dropdown";
|
||||
wrap.appendChild(dropdown);
|
||||
|
||||
// Kick off lazy fetch
|
||||
fetchCommands(apiUrl).then(function (list) {
|
||||
commands = list;
|
||||
});
|
||||
|
||||
function show(matches) {
|
||||
items = matches;
|
||||
selIdx = -1;
|
||||
if (!matches.length) {
|
||||
hide();
|
||||
return;
|
||||
}
|
||||
var fragment = document.createDocumentFragment();
|
||||
for (var i = 0; i < matches.length; i++) {
|
||||
var m = matches[i];
|
||||
var summ = m.cmd.summary || m.cmd.description || "";
|
||||
if (summ.length > 60) summ = summ.substring(0, 57) + "…";
|
||||
var item = document.createElement("div");
|
||||
item.className = "nb-ac-item";
|
||||
item.setAttribute("data-idx", String(i));
|
||||
|
||||
var cmd = document.createElement("span");
|
||||
cmd.className = "nb-ac-cmd";
|
||||
cmd.textContent = m.name;
|
||||
item.appendChild(cmd);
|
||||
|
||||
var args = document.createElement("span");
|
||||
args.className = "nb-ac-args";
|
||||
args.textContent = m.cmd.args_str || "";
|
||||
item.appendChild(args);
|
||||
|
||||
if (summ) {
|
||||
var summary = document.createElement("span");
|
||||
summary.className = "nb-ac-summ";
|
||||
summary.textContent = summ;
|
||||
item.appendChild(summary);
|
||||
}
|
||||
|
||||
fragment.appendChild(item);
|
||||
}
|
||||
dropdown.replaceChildren(fragment);
|
||||
dropdown.style.display = "block";
|
||||
isOpen = true;
|
||||
notifyResize();
|
||||
}
|
||||
|
||||
function hide() {
|
||||
if (!isOpen) return;
|
||||
dropdown.style.display = "none";
|
||||
dropdown.replaceChildren();
|
||||
items = [];
|
||||
selIdx = -1;
|
||||
isOpen = false;
|
||||
notifyResize();
|
||||
}
|
||||
|
||||
function selectItem(idx) {
|
||||
if (idx < 0 || idx >= items.length) return;
|
||||
var val = input.value;
|
||||
var firstSpace = val.indexOf(" ");
|
||||
// Replace the first token (command) with selected command
|
||||
if (firstSpace >= 0) {
|
||||
input.value = items[idx].name + val.substring(firstSpace);
|
||||
} else {
|
||||
input.value = items[idx].name;
|
||||
}
|
||||
hide();
|
||||
input.focus();
|
||||
input.setSelectionRange(input.value.length, input.value.length);
|
||||
}
|
||||
|
||||
function highlight(idx) {
|
||||
var ch = dropdown.children;
|
||||
for (var i = 0; i < ch.length; i++) {
|
||||
ch[i].classList.toggle("nb-ac-sel", i === idx);
|
||||
}
|
||||
selIdx = idx;
|
||||
if (idx >= 0 && ch[idx]) {
|
||||
ch[idx].scrollIntoView({ block: "nearest" });
|
||||
}
|
||||
}
|
||||
|
||||
function filter(text) {
|
||||
if (!commands || !text) {
|
||||
hide();
|
||||
return;
|
||||
}
|
||||
// Use only the first token being typed
|
||||
var first = text.split(/\s+/)[0].toLowerCase();
|
||||
if (first.length < minChars) {
|
||||
hide();
|
||||
return;
|
||||
}
|
||||
// If input already exactly matches a command AND there are args after it, don't show
|
||||
if (text.indexOf(" ") >= 0) {
|
||||
for (var c = 0; c < commands.length; c++) {
|
||||
if (commands[c].name === first) {
|
||||
hide();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Prefix matches first, then substring matches
|
||||
var prefix = [],
|
||||
substr = [];
|
||||
for (var i = 0; i < commands.length; i++) {
|
||||
var nm = commands[i].name.toLowerCase();
|
||||
if (nm.indexOf(first) === 0) {
|
||||
prefix.push(commands[i]);
|
||||
} else if (nm.indexOf(first) > 0) {
|
||||
substr.push(commands[i]);
|
||||
}
|
||||
}
|
||||
var results = prefix.concat(substr);
|
||||
if (results.length > maxResults) results = results.slice(0, maxResults);
|
||||
// If only exact match remains, hide autocomplete
|
||||
if (
|
||||
results.length === 1 &&
|
||||
results[0].name === first &&
|
||||
text.indexOf(" ") < 0
|
||||
) {
|
||||
hide();
|
||||
return;
|
||||
}
|
||||
show(results);
|
||||
}
|
||||
|
||||
function notifyResize() {
|
||||
if (global.parent !== global) {
|
||||
var h = Math.max(
|
||||
document.body.scrollHeight,
|
||||
document.documentElement.scrollHeight,
|
||||
);
|
||||
global.parent.postMessage(
|
||||
{ type: "iframe-resize", height: h + 10 },
|
||||
"*",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Events ────────────────────────────────────────── */
|
||||
|
||||
input.addEventListener("input", function () {
|
||||
filter(input.value.trim());
|
||||
});
|
||||
|
||||
input.addEventListener("focus", function () {
|
||||
var v = input.value.trim();
|
||||
if (v.length >= minChars) filter(v);
|
||||
});
|
||||
|
||||
input.addEventListener("keydown", function (e) {
|
||||
if (!isOpen) return;
|
||||
|
||||
if (e.key === "ArrowDown") {
|
||||
e.preventDefault();
|
||||
var next = selIdx + 1;
|
||||
if (next >= items.length) next = 0;
|
||||
highlight(next);
|
||||
} else if (e.key === "ArrowUp") {
|
||||
e.preventDefault();
|
||||
var prev = selIdx - 1;
|
||||
if (prev < 0) prev = items.length - 1;
|
||||
highlight(prev);
|
||||
} else if (e.key === "Tab") {
|
||||
e.preventDefault();
|
||||
if (selIdx >= 0) {
|
||||
selectItem(selIdx);
|
||||
} else if (items.length > 0) {
|
||||
selectItem(0);
|
||||
}
|
||||
} else if (e.key === "Enter" && !e.ctrlKey) {
|
||||
if (selIdx >= 0) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
selectItem(selIdx);
|
||||
}
|
||||
// If nothing highlighted, let Enter submit the form normally
|
||||
} else if (e.key === "Escape") {
|
||||
e.preventDefault();
|
||||
hide();
|
||||
}
|
||||
});
|
||||
|
||||
// Click on item
|
||||
dropdown.addEventListener("mousedown", function (e) {
|
||||
e.preventDefault(); // don't steal focus
|
||||
var target = e.target;
|
||||
while (target && !target.classList.contains("nb-ac-item")) {
|
||||
target = target.parentNode;
|
||||
if (target === dropdown) {
|
||||
target = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (target) {
|
||||
var idx = parseInt(target.getAttribute("data-idx"), 10);
|
||||
selectItem(idx);
|
||||
}
|
||||
});
|
||||
|
||||
// Hide on blur (with delay for click handling)
|
||||
input.addEventListener("blur", function () {
|
||||
setTimeout(function () {
|
||||
hide();
|
||||
}, 200);
|
||||
});
|
||||
|
||||
return { hide: hide, filter: filter };
|
||||
}
|
||||
|
||||
/* ── Export ──────────────────────────────────────────── */
|
||||
|
||||
global.NotebookAutocomplete = {
|
||||
attach: attach,
|
||||
fetchCommands: fetchCommands,
|
||||
};
|
||||
})(window, document);
|
||||
@@ -0,0 +1,38 @@
|
||||
.code-editor, .code-lines {
|
||||
font-family: lucida console, courier new, courier, monospace;
|
||||
margin: 0;
|
||||
height: 75vh;
|
||||
border-radius: 0;
|
||||
resize: none;
|
||||
line-height: 1.2;
|
||||
outline: none;
|
||||
-moz-box-sizing: border-box;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
font-family: Inconsolata;
|
||||
}
|
||||
.code-lines:focus-visible,
|
||||
.code-editor:focus-visible {
|
||||
outline:none;
|
||||
}
|
||||
.code-lines {
|
||||
display: flex;
|
||||
border-color: transparent;
|
||||
overflow-y: hidden;
|
||||
text-align: right;
|
||||
box-shadow: none;
|
||||
color: #707070;
|
||||
background-color: #d8d8d8;
|
||||
position: absolute;
|
||||
width: 3.5rem;
|
||||
background-color:#3E3D32;
|
||||
border-color:#3E3D32;
|
||||
color:#928869;
|
||||
}
|
||||
.code-editor {
|
||||
padding-left: calc(3.5rem + 5px);
|
||||
width:100%;
|
||||
background-color:#272822;
|
||||
border-color:#272822;
|
||||
color:#ffffff;
|
||||
}
|
||||
@@ -0,0 +1,838 @@
|
||||
(function (global, document) {
|
||||
"use strict";
|
||||
|
||||
var state = {
|
||||
activeIndex: -1,
|
||||
controller: null,
|
||||
mode: "literal",
|
||||
requestId: 0,
|
||||
results: [],
|
||||
timer: null,
|
||||
};
|
||||
|
||||
function cfg() {
|
||||
return global.NB_NOTEBOOK_SEARCH || {};
|
||||
}
|
||||
|
||||
function panel() {
|
||||
return document.getElementById("notebook-search");
|
||||
}
|
||||
|
||||
function toolbarButton() {
|
||||
return document.getElementById("open-notebook-search");
|
||||
}
|
||||
|
||||
function input() {
|
||||
return document.getElementById("notebook-search-focus");
|
||||
}
|
||||
|
||||
function resultsList() {
|
||||
return document.getElementById("notebook-search-results");
|
||||
}
|
||||
|
||||
function statusBox() {
|
||||
return document.getElementById("notebook-search-status");
|
||||
}
|
||||
|
||||
function surfaceSelect() {
|
||||
return document.getElementById("notebook-search-surface");
|
||||
}
|
||||
|
||||
function cellTypeSelect() {
|
||||
return document.getElementById("notebook-search-cell-type");
|
||||
}
|
||||
|
||||
function caseCheckbox() {
|
||||
return document.getElementById("notebook-search-case");
|
||||
}
|
||||
|
||||
function modeButtons() {
|
||||
return Array.prototype.slice.call(
|
||||
document.querySelectorAll("#notebook-search-mode-group [data-mode]"),
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeText(text) {
|
||||
return String(text || "").replace(/\r\n?/g, "\n");
|
||||
}
|
||||
|
||||
function clampIndex(index) {
|
||||
if (!state.results.length) return -1;
|
||||
if (index < 0) return 0;
|
||||
if (index >= state.results.length) return state.results.length - 1;
|
||||
return index;
|
||||
}
|
||||
|
||||
function searchDefaults() {
|
||||
var searchConfig = cfg().config || {};
|
||||
return {
|
||||
query: "",
|
||||
mode: searchConfig.default_mode || "literal",
|
||||
surface: searchConfig.default_surface || "all",
|
||||
cellType: searchConfig.default_cell_type || "all",
|
||||
caseSensitive: false,
|
||||
};
|
||||
}
|
||||
|
||||
function readStoredState() {
|
||||
var defaults = searchDefaults();
|
||||
try {
|
||||
if (!cfg().storageKey || !global.localStorage) {
|
||||
return defaults;
|
||||
}
|
||||
var raw = global.localStorage.getItem(cfg().storageKey);
|
||||
if (!raw) {
|
||||
return defaults;
|
||||
}
|
||||
var parsed = JSON.parse(raw);
|
||||
return {
|
||||
query: String(parsed.query || defaults.query),
|
||||
mode: String(parsed.mode || defaults.mode),
|
||||
surface: String(parsed.surface || defaults.surface),
|
||||
cellType: String(parsed.cellType || defaults.cellType),
|
||||
caseSensitive: !!parsed.caseSensitive,
|
||||
};
|
||||
} catch (error) {
|
||||
return defaults;
|
||||
}
|
||||
}
|
||||
|
||||
function writeStoredState() {
|
||||
try {
|
||||
if (!cfg().storageKey || !global.localStorage) {
|
||||
return;
|
||||
}
|
||||
global.localStorage.setItem(
|
||||
cfg().storageKey,
|
||||
JSON.stringify({
|
||||
query: input() ? input().value : "",
|
||||
mode: state.mode,
|
||||
surface: surfaceSelect() ? surfaceSelect().value : "all",
|
||||
cellType: cellTypeSelect() ? cellTypeSelect().value : "all",
|
||||
caseSensitive: caseCheckbox() ? caseCheckbox().checked : false,
|
||||
}),
|
||||
);
|
||||
} catch (error) {}
|
||||
}
|
||||
|
||||
function setMode(mode) {
|
||||
state.mode = mode || "literal";
|
||||
modeButtons().forEach(function (button) {
|
||||
button.classList.toggle("is-active", button.getAttribute("data-mode") === state.mode);
|
||||
});
|
||||
}
|
||||
|
||||
function currentLimit() {
|
||||
var searchConfig = cfg().config || {};
|
||||
var maxResults = Number(searchConfig.max_results || 0);
|
||||
if (!maxResults || maxResults < 1) {
|
||||
return 200;
|
||||
}
|
||||
return maxResults;
|
||||
}
|
||||
|
||||
function normalizeRanges(ranges, length) {
|
||||
if (!ranges || !ranges.length) return [];
|
||||
return ranges
|
||||
.map(function (range) {
|
||||
return {
|
||||
start: Math.max(0, Math.min(length, Number(range.start) || 0)),
|
||||
end: Math.max(0, Math.min(length, Number(range.end) || 0)),
|
||||
};
|
||||
})
|
||||
.filter(function (range) {
|
||||
return range.end > range.start;
|
||||
})
|
||||
.sort(function (a, b) {
|
||||
return a.start - b.start;
|
||||
});
|
||||
}
|
||||
|
||||
function unwrapMarks(root) {
|
||||
if (!root) return;
|
||||
var marks = root.querySelectorAll("mark.nb-search-hit");
|
||||
for (var i = 0; i < marks.length; i++) {
|
||||
var mark = marks[i];
|
||||
var parent = mark.parentNode;
|
||||
while (mark.firstChild) {
|
||||
parent.insertBefore(mark.firstChild, mark);
|
||||
}
|
||||
parent.removeChild(mark);
|
||||
}
|
||||
root.normalize();
|
||||
}
|
||||
|
||||
function codePointToCodeUnitOffset(text, codePointOffset) {
|
||||
if (codePointOffset <= 0) return 0;
|
||||
return Array.from(text).slice(0, codePointOffset).join("").length;
|
||||
}
|
||||
|
||||
function childOffset(node) {
|
||||
var offset = 0;
|
||||
while (node && node.previousSibling) {
|
||||
node = node.previousSibling;
|
||||
offset++;
|
||||
}
|
||||
return offset;
|
||||
}
|
||||
|
||||
function buildTextSegments(root, includeBreaks) {
|
||||
var segments = [];
|
||||
var codePointIndex = 0;
|
||||
|
||||
function walk(node) {
|
||||
if (!node) return;
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
var length = Array.from(node.data || "").length;
|
||||
if (!length) return;
|
||||
segments.push({
|
||||
type: "text",
|
||||
node: node,
|
||||
start: codePointIndex,
|
||||
end: codePointIndex + length,
|
||||
});
|
||||
codePointIndex += length;
|
||||
return;
|
||||
}
|
||||
if (node.nodeType !== Node.ELEMENT_NODE) {
|
||||
return;
|
||||
}
|
||||
if (includeBreaks && node.tagName === "BR") {
|
||||
segments.push({
|
||||
type: "break",
|
||||
parent: node.parentNode,
|
||||
offset: childOffset(node),
|
||||
start: codePointIndex,
|
||||
end: codePointIndex + 1,
|
||||
});
|
||||
codePointIndex++;
|
||||
return;
|
||||
}
|
||||
for (var child = node.firstChild; child; child = child.nextSibling) {
|
||||
walk(child);
|
||||
}
|
||||
}
|
||||
|
||||
walk(root);
|
||||
return segments;
|
||||
}
|
||||
|
||||
function boundaryBeforeSegment(segment) {
|
||||
if (!segment) return null;
|
||||
if (segment.type === "break") {
|
||||
return { container: segment.parent, offset: segment.offset };
|
||||
}
|
||||
return { container: segment.node, offset: 0 };
|
||||
}
|
||||
|
||||
function boundaryAfterSegment(segment) {
|
||||
if (!segment) return null;
|
||||
if (segment.type === "break") {
|
||||
return { container: segment.parent, offset: segment.offset + 1 };
|
||||
}
|
||||
return {
|
||||
container: segment.node,
|
||||
offset: codePointToCodeUnitOffset(segment.node.data || "", segment.end - segment.start),
|
||||
};
|
||||
}
|
||||
|
||||
function locateBoundary(root, segments, absoluteOffset) {
|
||||
if (!segments.length) {
|
||||
return { container: root, offset: 0 };
|
||||
}
|
||||
|
||||
for (var i = 0; i < segments.length; i++) {
|
||||
var segment = segments[i];
|
||||
if (absoluteOffset < segment.start) {
|
||||
return boundaryBeforeSegment(segment);
|
||||
}
|
||||
if (segment.type === "break") {
|
||||
if (absoluteOffset === segment.start) {
|
||||
return boundaryBeforeSegment(segment);
|
||||
}
|
||||
if (absoluteOffset === segment.end) {
|
||||
return boundaryAfterSegment(segment);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (absoluteOffset <= segment.end) {
|
||||
return {
|
||||
container: segment.node,
|
||||
offset: codePointToCodeUnitOffset(segment.node.data || "", absoluteOffset - segment.start),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return boundaryAfterSegment(segments[segments.length - 1]);
|
||||
}
|
||||
|
||||
function wrapCodePointRange(root, start, end, includeBreaks) {
|
||||
var segments = buildTextSegments(root, includeBreaks);
|
||||
var startBoundary = locateBoundary(root, segments, start);
|
||||
var endBoundary = locateBoundary(root, segments, end);
|
||||
if (!startBoundary || !endBoundary) return null;
|
||||
|
||||
var range = document.createRange();
|
||||
range.setStart(startBoundary.container, startBoundary.offset);
|
||||
range.setEnd(endBoundary.container, endBoundary.offset);
|
||||
if (range.collapsed) return null;
|
||||
|
||||
var mark = document.createElement("mark");
|
||||
mark.className = "nb-search-hit";
|
||||
mark.appendChild(range.extractContents());
|
||||
range.insertNode(mark);
|
||||
return mark;
|
||||
}
|
||||
|
||||
function lineBounds(text, lineNumber) {
|
||||
if (lineNumber <= 0) return null;
|
||||
var chars = Array.from(normalizeText(text));
|
||||
var start = 0;
|
||||
var currentLine = 1;
|
||||
while (start < chars.length && currentLine < lineNumber) {
|
||||
if (chars[start] === "\n") {
|
||||
currentLine++;
|
||||
}
|
||||
start++;
|
||||
}
|
||||
if (currentLine !== lineNumber) {
|
||||
return null;
|
||||
}
|
||||
var end = start;
|
||||
while (end < chars.length && chars[end] !== "\n") {
|
||||
end++;
|
||||
}
|
||||
return { start: start, end: end };
|
||||
}
|
||||
|
||||
function highlightInlineRanges(root, fullText, lineNumber, ranges, includeBreaks) {
|
||||
if (!root) return null;
|
||||
unwrapMarks(root);
|
||||
|
||||
var bounds = lineBounds(fullText, Number(lineNumber || 0));
|
||||
if (!bounds) return null;
|
||||
|
||||
var safeRanges = normalizeRanges(ranges, bounds.end - bounds.start).sort(function (a, b) {
|
||||
return b.start - a.start;
|
||||
});
|
||||
var firstMark = null;
|
||||
for (var i = 0; i < safeRanges.length; i++) {
|
||||
var range = safeRanges[i];
|
||||
var mark = wrapCodePointRange(
|
||||
root,
|
||||
bounds.start + range.start,
|
||||
bounds.start + range.end,
|
||||
includeBreaks,
|
||||
);
|
||||
if (mark) {
|
||||
firstMark = mark;
|
||||
}
|
||||
}
|
||||
return firstMark;
|
||||
}
|
||||
|
||||
function buildHighlightedTextFragment(text, ranges) {
|
||||
var fragment = document.createDocumentFragment();
|
||||
var chars = Array.from(String(text || ""));
|
||||
var safeRanges = normalizeRanges(ranges, chars.length);
|
||||
if (!safeRanges.length) {
|
||||
if (chars.length) {
|
||||
fragment.appendChild(document.createTextNode(chars.join("")));
|
||||
}
|
||||
return fragment;
|
||||
}
|
||||
|
||||
var cursor = 0;
|
||||
safeRanges.forEach(function (range) {
|
||||
if (cursor < range.start) {
|
||||
fragment.appendChild(document.createTextNode(chars.slice(cursor, range.start).join("")));
|
||||
}
|
||||
var mark = document.createElement("mark");
|
||||
mark.className = "nb-search-hit";
|
||||
mark.textContent = chars.slice(range.start, range.end).join("");
|
||||
fragment.appendChild(mark);
|
||||
cursor = range.end;
|
||||
});
|
||||
if (cursor < chars.length) {
|
||||
fragment.appendChild(document.createTextNode(chars.slice(cursor).join("")));
|
||||
}
|
||||
return fragment;
|
||||
}
|
||||
|
||||
function resultSurfaceLabel(result) {
|
||||
if (result.surface === "output") {
|
||||
return "Output";
|
||||
}
|
||||
if (result.cell_type === "markdown") {
|
||||
return "Markdown";
|
||||
}
|
||||
return "Source";
|
||||
}
|
||||
|
||||
function resultCellTypeLabel(result) {
|
||||
if (result.cell_type === "script") {
|
||||
return "Script";
|
||||
}
|
||||
if (result.cell_type === "markdown") {
|
||||
return "Markdown";
|
||||
}
|
||||
return "Command";
|
||||
}
|
||||
|
||||
function setStatus(text, isError) {
|
||||
var box = statusBox();
|
||||
if (!box) return;
|
||||
box.textContent = text;
|
||||
box.classList.toggle("is-error", !!isError);
|
||||
}
|
||||
|
||||
function clearResults() {
|
||||
var list = resultsList();
|
||||
if (list) {
|
||||
list.replaceChildren();
|
||||
}
|
||||
state.results = [];
|
||||
state.activeIndex = -1;
|
||||
}
|
||||
|
||||
function showEmptyState(message) {
|
||||
clearResults();
|
||||
setStatus(message, false);
|
||||
}
|
||||
|
||||
function updateToolbarState() {
|
||||
var button = toolbarButton();
|
||||
if (!button) return;
|
||||
button.classList.toggle("is-active", isNotebookSearchOpen());
|
||||
}
|
||||
|
||||
function isNotebookSearchOpen() {
|
||||
var currentPanel = panel();
|
||||
return !!currentPanel && currentPanel.classList.contains("is-open");
|
||||
}
|
||||
|
||||
function openNotebookSearch() {
|
||||
if (global.isCommandHelpOpen && global.isCommandHelpOpen()) {
|
||||
global.closeCmdHelp();
|
||||
}
|
||||
|
||||
var currentPanel = panel();
|
||||
if (!currentPanel) return;
|
||||
currentPanel.classList.add("is-open");
|
||||
currentPanel.setAttribute("aria-hidden", "false");
|
||||
updateToolbarState();
|
||||
|
||||
var searchInput = input();
|
||||
if (!searchInput) return;
|
||||
searchInput.focus();
|
||||
searchInput.select();
|
||||
if (searchInput.value.trim()) {
|
||||
queueNotebookSearch(0);
|
||||
return;
|
||||
}
|
||||
showEmptyState("Search commands, scripts, outputs, and markdown in this notebook.");
|
||||
}
|
||||
|
||||
function closeNotebookSearch() {
|
||||
var currentPanel = panel();
|
||||
if (!currentPanel) return;
|
||||
currentPanel.classList.remove("is-open");
|
||||
currentPanel.setAttribute("aria-hidden", "true");
|
||||
updateToolbarState();
|
||||
}
|
||||
|
||||
function queueNotebookSearch(delay) {
|
||||
if (state.timer) {
|
||||
global.clearTimeout(state.timer);
|
||||
}
|
||||
state.timer = global.setTimeout(runNotebookSearch, typeof delay === "number" ? delay : 140);
|
||||
}
|
||||
|
||||
function setActiveResult(index, shouldScroll) {
|
||||
state.activeIndex = clampIndex(index);
|
||||
var buttons = document.querySelectorAll(".nb-search-result");
|
||||
for (var i = 0; i < buttons.length; i++) {
|
||||
var isActive = i === state.activeIndex;
|
||||
buttons[i].classList.toggle("is-active", isActive);
|
||||
buttons[i].setAttribute("aria-selected", isActive ? "true" : "false");
|
||||
if (isActive && shouldScroll && buttons[i].scrollIntoView) {
|
||||
buttons[i].scrollIntoView({ block: "nearest" });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function moveActiveResult(delta) {
|
||||
if (!state.results.length) return;
|
||||
var nextIndex = state.activeIndex;
|
||||
if (nextIndex < 0) {
|
||||
nextIndex = 0;
|
||||
} else {
|
||||
nextIndex = (nextIndex + delta + state.results.length) % state.results.length;
|
||||
}
|
||||
setActiveResult(nextIndex, true);
|
||||
}
|
||||
|
||||
function runNotebookSearch() {
|
||||
state.timer = null;
|
||||
|
||||
var searchInput = input();
|
||||
var query = searchInput ? searchInput.value.trim() : "";
|
||||
if (state.controller) {
|
||||
state.controller.abort();
|
||||
state.controller = null;
|
||||
}
|
||||
writeStoredState();
|
||||
if (!query) {
|
||||
showEmptyState("Search commands, scripts, outputs, and markdown in this notebook.");
|
||||
return;
|
||||
}
|
||||
|
||||
var requestId = ++state.requestId;
|
||||
var params = new URLSearchParams();
|
||||
params.set("query", query);
|
||||
params.set("mode", state.mode);
|
||||
params.set("surface", surfaceSelect() ? surfaceSelect().value : "all");
|
||||
params.set("cell_type", cellTypeSelect() ? cellTypeSelect().value : "all");
|
||||
params.set("limit", String(currentLimit()));
|
||||
if (caseCheckbox() && caseCheckbox().checked) {
|
||||
params.set("case_sensitive", "1");
|
||||
}
|
||||
|
||||
setStatus("Searching this notebook...", false);
|
||||
|
||||
var controller = null;
|
||||
if (global.AbortController) {
|
||||
controller = new global.AbortController();
|
||||
state.controller = controller;
|
||||
}
|
||||
|
||||
global
|
||||
.fetch(cfg().searchEndpoint + "?" + params.toString(), {
|
||||
headers: { Accept: "application/json" },
|
||||
signal: controller ? controller.signal : undefined,
|
||||
})
|
||||
.then(function (response) {
|
||||
return response.json().then(function (body) {
|
||||
return { ok: response.ok, body: body };
|
||||
});
|
||||
})
|
||||
.then(function (payload) {
|
||||
if (state.controller === controller) {
|
||||
state.controller = null;
|
||||
}
|
||||
if (requestId !== state.requestId) return;
|
||||
if (!payload.ok) {
|
||||
clearResults();
|
||||
setStatus(payload.body.error || "Search failed.", true);
|
||||
return;
|
||||
}
|
||||
renderNotebookSearchResults(payload.body);
|
||||
})
|
||||
.catch(function (error) {
|
||||
if (state.controller === controller) {
|
||||
state.controller = null;
|
||||
}
|
||||
if (error && error.name === "AbortError") {
|
||||
return;
|
||||
}
|
||||
if (requestId !== state.requestId) return;
|
||||
clearResults();
|
||||
setStatus(error && error.message ? error.message : "Search failed.", true);
|
||||
});
|
||||
}
|
||||
|
||||
function renderNotebookSearchResults(response) {
|
||||
var list = resultsList();
|
||||
if (!list) return;
|
||||
|
||||
list.replaceChildren();
|
||||
state.results = response.results || [];
|
||||
state.activeIndex = state.results.length ? 0 : -1;
|
||||
|
||||
if (!state.results.length) {
|
||||
setStatus("No matches found in this notebook.", false);
|
||||
return;
|
||||
}
|
||||
|
||||
setStatus(
|
||||
response.truncated
|
||||
? "Showing " + state.results.length + " of " + response.total + " matches."
|
||||
: response.total + " matches found.",
|
||||
false,
|
||||
);
|
||||
|
||||
var fragment = document.createDocumentFragment();
|
||||
state.results.forEach(function (result, index) {
|
||||
var item = document.createElement("li");
|
||||
item.className = "nb-search-result-item";
|
||||
|
||||
var button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = "nb-search-result" + (index === 0 ? " is-active" : "");
|
||||
button.setAttribute("aria-selected", index === 0 ? "true" : "false");
|
||||
button.addEventListener("click", function () {
|
||||
jumpToNotebookSearchResult(index);
|
||||
});
|
||||
button.addEventListener("mouseenter", function () {
|
||||
setActiveResult(index, false);
|
||||
});
|
||||
|
||||
var meta = document.createElement("div");
|
||||
meta.className = "nb-search-result-meta";
|
||||
appendMetaBadge(meta, resultSurfaceLabel(result), "nb-search-badge" + (result.surface === "output" ? " is-output" : ""));
|
||||
appendMetaBadge(meta, resultCellTypeLabel(result), "nb-search-badge");
|
||||
if (response.mode === "fuzzy") {
|
||||
appendMetaBadge(meta, "score " + String(result.score || 0), "nb-search-badge is-fuzzy");
|
||||
}
|
||||
appendMetaText(meta, "cell " + String(Number(result.cell_position || 0) + 1));
|
||||
appendMetaText(meta, "line " + String(result.line_number || 0));
|
||||
appendMetaText(meta, "cols " + String(result.start_column || 0) + "-" + String(result.end_column || 0));
|
||||
button.appendChild(meta);
|
||||
|
||||
var line = document.createElement("div");
|
||||
line.className = "nb-search-result-line";
|
||||
line.appendChild(buildHighlightedTextFragment(result.line_text, result.ranges));
|
||||
button.appendChild(line);
|
||||
|
||||
item.appendChild(button);
|
||||
fragment.appendChild(item);
|
||||
});
|
||||
list.appendChild(fragment);
|
||||
}
|
||||
|
||||
function appendMetaBadge(parent, text, className) {
|
||||
var badge = document.createElement("span");
|
||||
badge.className = className;
|
||||
badge.textContent = text;
|
||||
parent.appendChild(badge);
|
||||
}
|
||||
|
||||
function appendMetaText(parent, text) {
|
||||
var span = document.createElement("span");
|
||||
span.textContent = text;
|
||||
parent.appendChild(span);
|
||||
}
|
||||
|
||||
function clearNotebookSearchHighlights() {
|
||||
Array.prototype.slice
|
||||
.call(document.querySelectorAll(".cell-group.nb-search-active-cell"))
|
||||
.forEach(function (cell) {
|
||||
cell.classList.remove("nb-search-active-cell");
|
||||
});
|
||||
|
||||
Array.prototype.slice
|
||||
.call(document.querySelectorAll(".nb-searchable-code"))
|
||||
.forEach(function (code) {
|
||||
unwrapMarks(code);
|
||||
});
|
||||
|
||||
Array.prototype.slice
|
||||
.call(document.querySelectorAll('iframe[id^="output-"], iframe[id^="markdown-"]'))
|
||||
.forEach(function (iframe) {
|
||||
try {
|
||||
iframe.contentWindow.postMessage({ type: "notebook-search-clear" }, "*");
|
||||
} catch (error) {}
|
||||
});
|
||||
}
|
||||
|
||||
function highlightSearchableCodeBlock(code, lineNumber, ranges) {
|
||||
if (!code) return;
|
||||
var target = highlightInlineRanges(
|
||||
code,
|
||||
normalizeText(code.textContent || ""),
|
||||
lineNumber,
|
||||
ranges || [],
|
||||
false,
|
||||
);
|
||||
if (target && target.scrollIntoView) {
|
||||
target.scrollIntoView({ block: "center", behavior: "smooth" });
|
||||
}
|
||||
}
|
||||
|
||||
function scrollToCell(cellId) {
|
||||
var group = document.getElementById("cell-" + cellId);
|
||||
if (!group) return null;
|
||||
group.classList.add("nb-search-active-cell");
|
||||
if (group.scrollIntoView) {
|
||||
group.scrollIntoView({ block: "center", behavior: "smooth" });
|
||||
}
|
||||
return group;
|
||||
}
|
||||
|
||||
function serializeRanges(ranges) {
|
||||
return (ranges || [])
|
||||
.map(function (range) {
|
||||
return range.start + ":" + range.end;
|
||||
})
|
||||
.join(",");
|
||||
}
|
||||
|
||||
function openMarkdownSearchResult(result) {
|
||||
var iframe = document.getElementById("markdown-" + result.cell_id);
|
||||
if (!iframe) return;
|
||||
|
||||
if (iframe.src && iframe.src.indexOf("/markdown/edit/") >= 0) {
|
||||
try {
|
||||
iframe.contentWindow.postMessage(
|
||||
{
|
||||
type: "notebook-search-highlight-editor",
|
||||
lineNumber: result.line_number,
|
||||
ranges: result.ranges || [],
|
||||
},
|
||||
"*",
|
||||
);
|
||||
return;
|
||||
} catch (error) {}
|
||||
}
|
||||
|
||||
var url = new URL(cfg().markdownEditBase + result.cell_id, global.location.href);
|
||||
url.searchParams.set("search_line", String(result.line_number));
|
||||
url.searchParams.set("search_ranges", serializeRanges(result.ranges));
|
||||
url.searchParams.set("search_request", String(Date.now()));
|
||||
iframe.src = url.toString();
|
||||
}
|
||||
|
||||
function highlightOutputResult(result) {
|
||||
var iframe = document.getElementById("output-" + result.cell_id);
|
||||
if (!iframe) return;
|
||||
|
||||
var message = {
|
||||
type: "notebook-search-highlight",
|
||||
lineNumber: result.line_number,
|
||||
ranges: result.ranges || [],
|
||||
};
|
||||
try {
|
||||
iframe.contentWindow.postMessage(message, "*");
|
||||
global.setTimeout(function () {
|
||||
try {
|
||||
iframe.contentWindow.postMessage(message, "*");
|
||||
} catch (error) {}
|
||||
}, 90);
|
||||
global.setTimeout(function () {
|
||||
try {
|
||||
iframe.contentWindow.postMessage(message, "*");
|
||||
} catch (error) {}
|
||||
}, 240);
|
||||
} catch (error) {}
|
||||
}
|
||||
|
||||
function jumpToNotebookSearchResult(index) {
|
||||
var result = state.results[index];
|
||||
if (!result) return;
|
||||
|
||||
setActiveResult(index, true);
|
||||
clearNotebookSearchHighlights();
|
||||
scrollToCell(result.cell_id);
|
||||
|
||||
if (result.cell_type === "markdown") {
|
||||
openMarkdownSearchResult(result);
|
||||
return;
|
||||
}
|
||||
if (result.surface === "output") {
|
||||
highlightOutputResult(result);
|
||||
return;
|
||||
}
|
||||
highlightSearchableCodeBlock(
|
||||
document.getElementById("content-" + result.cell_id),
|
||||
result.line_number,
|
||||
result.ranges || [],
|
||||
);
|
||||
}
|
||||
|
||||
function applyInitialState() {
|
||||
var stored = readStoredState();
|
||||
var searchInput = input();
|
||||
if (searchInput) {
|
||||
searchInput.value = stored.query;
|
||||
}
|
||||
if (surfaceSelect()) {
|
||||
surfaceSelect().value = stored.surface;
|
||||
}
|
||||
if (cellTypeSelect()) {
|
||||
cellTypeSelect().value = stored.cellType;
|
||||
}
|
||||
if (caseCheckbox()) {
|
||||
caseCheckbox().checked = !!stored.caseSensitive;
|
||||
}
|
||||
setMode(stored.mode);
|
||||
}
|
||||
|
||||
function bindNotebookSearchUI() {
|
||||
var searchInput = input();
|
||||
var searchCloseButton = document.getElementById("close-notebook-search");
|
||||
|
||||
applyInitialState();
|
||||
updateToolbarState();
|
||||
|
||||
if (toolbarButton()) {
|
||||
toolbarButton().addEventListener("click", function () {
|
||||
if (isNotebookSearchOpen()) {
|
||||
closeNotebookSearch();
|
||||
} else {
|
||||
openNotebookSearch();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (searchCloseButton) {
|
||||
searchCloseButton.addEventListener("click", function () {
|
||||
closeNotebookSearch();
|
||||
});
|
||||
}
|
||||
|
||||
modeButtons().forEach(function (button) {
|
||||
button.addEventListener("click", function () {
|
||||
setMode(button.getAttribute("data-mode"));
|
||||
writeStoredState();
|
||||
queueNotebookSearch(0);
|
||||
});
|
||||
});
|
||||
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener("input", function () {
|
||||
writeStoredState();
|
||||
queueNotebookSearch();
|
||||
});
|
||||
searchInput.addEventListener("keydown", function (event) {
|
||||
if (event.key === "ArrowDown") {
|
||||
event.preventDefault();
|
||||
moveActiveResult(1);
|
||||
return;
|
||||
}
|
||||
if (event.key === "ArrowUp") {
|
||||
event.preventDefault();
|
||||
moveActiveResult(-1);
|
||||
return;
|
||||
}
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
if (state.activeIndex >= 0) {
|
||||
jumpToNotebookSearchResult(state.activeIndex);
|
||||
} else {
|
||||
runNotebookSearch();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (event.key === "Escape") {
|
||||
event.preventDefault();
|
||||
closeNotebookSearch();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[surfaceSelect(), cellTypeSelect(), caseCheckbox()].forEach(function (control) {
|
||||
if (!control) return;
|
||||
control.addEventListener("change", function () {
|
||||
writeStoredState();
|
||||
queueNotebookSearch(0);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
global.openNotebookSearch = openNotebookSearch;
|
||||
global.closeNotebookSearch = closeNotebookSearch;
|
||||
global.isNotebookSearchOpen = isNotebookSearchOpen;
|
||||
global.jumpToNotebookSearchResult = jumpToNotebookSearchResult;
|
||||
|
||||
document.addEventListener("DOMContentLoaded", bindNotebookSearchUI);
|
||||
})(window, document);
|
||||
@@ -0,0 +1,829 @@
|
||||
(function (global, document) {
|
||||
"use strict";
|
||||
|
||||
var isMac = /Mac|iPhone|iPad|iPod/.test(global.navigator.platform);
|
||||
var config = global.NOTEBOOK_KB || {};
|
||||
var defaults = {
|
||||
about: "alt+a",
|
||||
new_page: "ctrl+n",
|
||||
settings: "alt+s",
|
||||
toggle_pipe: "alt+o",
|
||||
new_command: "alt+c",
|
||||
new_markdown: "alt+m",
|
||||
new_script: "alt+j",
|
||||
search_notebook: "mod+shift+f",
|
||||
save: "mod+s",
|
||||
cancel: "escape",
|
||||
execute: "ctrl+enter",
|
||||
edit_markdown: "alt+e",
|
||||
};
|
||||
|
||||
function normalizeKey(key) {
|
||||
if (!key) return "";
|
||||
var normalized = String(key).toLowerCase();
|
||||
switch (normalized) {
|
||||
case " ":
|
||||
case "spacebar":
|
||||
return "space";
|
||||
case "esc":
|
||||
return "escape";
|
||||
case "arrowup":
|
||||
return "up";
|
||||
case "arrowdown":
|
||||
return "down";
|
||||
case "arrowleft":
|
||||
return "left";
|
||||
case "arrowright":
|
||||
return "right";
|
||||
default:
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
|
||||
function expandToken(token) {
|
||||
token = normalizeKey(token);
|
||||
if (token === "mod") {
|
||||
return isMac ? "meta" : "ctrl";
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
function parseCombo(combo) {
|
||||
var parts = String(combo || "")
|
||||
.split("+")
|
||||
.map(function (part) {
|
||||
return expandToken(part.trim());
|
||||
})
|
||||
.filter(Boolean);
|
||||
return {
|
||||
ctrl: parts.indexOf("ctrl") >= 0,
|
||||
meta: parts.indexOf("meta") >= 0,
|
||||
alt: parts.indexOf("alt") >= 0,
|
||||
shift: parts.indexOf("shift") >= 0,
|
||||
key:
|
||||
parts.filter(function (part) {
|
||||
return ["ctrl", "meta", "alt", "shift"].indexOf(part) < 0;
|
||||
})[0] || "",
|
||||
};
|
||||
}
|
||||
|
||||
function matches(event, combo) {
|
||||
var spec = parseCombo(combo);
|
||||
if (!spec.key) return false;
|
||||
if (!!event.ctrlKey !== spec.ctrl) return false;
|
||||
if (!!event.metaKey !== spec.meta) return false;
|
||||
if (!!event.altKey !== spec.alt) return false;
|
||||
if (!!event.shiftKey !== spec.shift) return false;
|
||||
return normalizeKey(event.key) === spec.key;
|
||||
}
|
||||
|
||||
function isEditableTarget(target) {
|
||||
if (!target || target === document.body) return false;
|
||||
if (target.isContentEditable) return true;
|
||||
var tag = (target.tagName || "").toLowerCase();
|
||||
return tag === "input" || tag === "textarea" || tag === "select";
|
||||
}
|
||||
|
||||
function formatCombo(combo) {
|
||||
if (!combo) return "";
|
||||
var tokens = String(combo)
|
||||
.split("+")
|
||||
.map(function (token) {
|
||||
token = token.trim().toLowerCase();
|
||||
if (token === "mod") return isMac ? "⌘" : "Ctrl";
|
||||
if (token === "ctrl") return isMac ? "⌃" : "Ctrl";
|
||||
if (token === "meta") return isMac ? "⌘" : "Meta";
|
||||
if (token === "alt") return isMac ? "⌥" : "Alt";
|
||||
if (token === "shift") return "⇧";
|
||||
if (token === "escape") return "Esc";
|
||||
if (token === "enter") return "↩";
|
||||
if (token === "space") return "Space";
|
||||
return token.length === 1
|
||||
? token.toUpperCase()
|
||||
: token.charAt(0).toUpperCase() + token.slice(1);
|
||||
});
|
||||
return tokens.join(isMac ? "" : "+");
|
||||
}
|
||||
|
||||
function combo(action) {
|
||||
return config[action] || defaults[action] || "";
|
||||
}
|
||||
|
||||
function bind(bindings) {
|
||||
document.addEventListener(
|
||||
"keydown",
|
||||
function (event) {
|
||||
if (!bindings || !bindings.length) return;
|
||||
for (var i = 0; i < bindings.length; i++) {
|
||||
var binding = bindings[i];
|
||||
if (!binding) continue;
|
||||
|
||||
var comboString = "";
|
||||
if (binding.action) {
|
||||
comboString = combo(binding.action);
|
||||
} else if (binding.combo) {
|
||||
comboString = String(binding.combo);
|
||||
}
|
||||
if (!comboString) continue;
|
||||
if (!matches(event, comboString)) continue;
|
||||
if (isEditableTarget(event.target) && !binding.allowInInputs) continue;
|
||||
if (binding.when && !binding.when(event)) continue;
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
binding.handler(event);
|
||||
return;
|
||||
}
|
||||
},
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
function annotate(element, actionOrCombo) {
|
||||
if (!element) return;
|
||||
var comboString = defaults[actionOrCombo] ? combo(actionOrCombo) : actionOrCombo;
|
||||
if (!comboString) return;
|
||||
var display = formatCombo(comboString);
|
||||
var text = (element.textContent || "").replace(/\s+/g, " ").trim();
|
||||
var tooltip = text ? text + " (" + display + ")" : display;
|
||||
element.setAttribute("data-tooltip", tooltip);
|
||||
element.setAttribute("aria-keyshortcuts", display);
|
||||
if (element.classList) {
|
||||
element.classList.add("tooltip");
|
||||
element.classList.add("tooltip-bottom");
|
||||
}
|
||||
}
|
||||
|
||||
function record(callback) {
|
||||
function handler(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
var parts = [];
|
||||
if (event.ctrlKey) parts.push("ctrl");
|
||||
if (event.altKey) parts.push("alt");
|
||||
if (event.shiftKey) parts.push("shift");
|
||||
if (event.metaKey) parts.push("meta");
|
||||
var key = normalizeKey(event.key);
|
||||
if (["control", "alt", "shift", "meta"].indexOf(key) >= 0) return;
|
||||
parts.push(key);
|
||||
document.removeEventListener("keydown", handler, true);
|
||||
if (callback) callback(parts.join("+"));
|
||||
}
|
||||
|
||||
document.addEventListener("keydown", handler, true);
|
||||
}
|
||||
|
||||
var ALIGN_VALUES = {
|
||||
center: true,
|
||||
justify: true,
|
||||
left: true,
|
||||
right: true,
|
||||
};
|
||||
|
||||
var CODE_CLASS_RE =
|
||||
/^(?:lang|language)-[A-Za-z0-9_-]+(?:\s+(?:lang|language)-[A-Za-z0-9_-]+)*$/;
|
||||
|
||||
var HTML_ENTITY_MAP = {
|
||||
amp: "&",
|
||||
apos: "'",
|
||||
gt: ">",
|
||||
lt: "<",
|
||||
nbsp: "\u00a0",
|
||||
quot: '"',
|
||||
};
|
||||
|
||||
function emptyFragment() {
|
||||
return document.createDocumentFragment();
|
||||
}
|
||||
|
||||
function normalizeUrlCandidate(url) {
|
||||
return String(url || "").replace(/[\u0000-\u001F\u007F-\u009F\s]+/g, "");
|
||||
}
|
||||
|
||||
function sanitizeUrl(url, options) {
|
||||
var value = String(url || "").trim();
|
||||
if (!value) {
|
||||
return "";
|
||||
}
|
||||
if (value.charAt(0) === "#") {
|
||||
return value;
|
||||
}
|
||||
|
||||
var normalized = normalizeUrlCandidate(value).toLowerCase();
|
||||
if (
|
||||
normalized.indexOf("javascript:") === 0 ||
|
||||
normalized.indexOf("vbscript:") === 0
|
||||
) {
|
||||
return "";
|
||||
}
|
||||
if (normalized.indexOf("data:") === 0) {
|
||||
if (
|
||||
options &&
|
||||
options.allowDataImage &&
|
||||
/^data:image\/(?:bmp|gif|jpe?g|png|webp);/i.test(value)
|
||||
) {
|
||||
return value;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
if (value.indexOf("//") === 0) {
|
||||
return value;
|
||||
}
|
||||
if (/^[A-Za-z][A-Za-z0-9+.-]*:/.test(normalized)) {
|
||||
var scheme = normalized.split(":", 1)[0];
|
||||
if (
|
||||
scheme === "ftp" ||
|
||||
scheme === "http" ||
|
||||
scheme === "https" ||
|
||||
scheme === "mailto"
|
||||
) {
|
||||
return value;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function applyTextAlign(target, value) {
|
||||
var normalized = String(value || "").trim().toLowerCase();
|
||||
if (ALIGN_VALUES[normalized]) {
|
||||
target.style.textAlign = normalized;
|
||||
}
|
||||
}
|
||||
|
||||
function copyTextAlign(source, target) {
|
||||
applyTextAlign(target, source.getAttribute("align"));
|
||||
|
||||
var style = source.getAttribute("style");
|
||||
if (!style) {
|
||||
return;
|
||||
}
|
||||
|
||||
style.split(";").forEach(function (declaration) {
|
||||
var parts = declaration.split(":");
|
||||
if (parts.length < 2) {
|
||||
return;
|
||||
}
|
||||
if (parts[0].trim().toLowerCase() === "text-align") {
|
||||
applyTextAlign(target, parts.slice(1).join(":"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function appendNode(target, node) {
|
||||
if (target && node) {
|
||||
target.appendChild(node);
|
||||
}
|
||||
}
|
||||
|
||||
function decodeEntities(text) {
|
||||
return String(text || "").replace(
|
||||
/&(?:#(\d+)|#x([0-9a-fA-F]+)|([A-Za-z][A-Za-z0-9]+));/g,
|
||||
function (match, dec, hex, named) {
|
||||
var value;
|
||||
if (dec) {
|
||||
value = parseInt(dec, 10);
|
||||
} else if (hex) {
|
||||
value = parseInt(hex, 16);
|
||||
} else if (named) {
|
||||
if (!Object.prototype.hasOwnProperty.call(HTML_ENTITY_MAP, named)) {
|
||||
return match;
|
||||
}
|
||||
return HTML_ENTITY_MAP[named];
|
||||
}
|
||||
if (!isFinite(value) || value < 0 || value > 1114111) {
|
||||
return match;
|
||||
}
|
||||
try {
|
||||
return String.fromCodePoint(value);
|
||||
} catch (error) {
|
||||
return match;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function appendDecodedText(target, text) {
|
||||
if (text) {
|
||||
appendNode(target, document.createTextNode(decodeEntities(text)));
|
||||
}
|
||||
}
|
||||
|
||||
function appendLiteralText(target, text) {
|
||||
if (text) {
|
||||
appendNode(target, document.createTextNode(String(text)));
|
||||
}
|
||||
}
|
||||
|
||||
function createMarkdownOptions(renderer) {
|
||||
var options = {
|
||||
breaks: true,
|
||||
gfm: true,
|
||||
pedantic: false,
|
||||
smartypants: false,
|
||||
tables: true,
|
||||
};
|
||||
var config =
|
||||
renderer &&
|
||||
renderer.options &&
|
||||
renderer.options.renderingConfig;
|
||||
if (config && config.singleLineBreaks === false) {
|
||||
options.breaks = false;
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
function getMarkedEngine(renderer) {
|
||||
if (
|
||||
renderer &&
|
||||
renderer.constructor &&
|
||||
renderer.constructor.marked
|
||||
) {
|
||||
return renderer.constructor.marked;
|
||||
}
|
||||
if (global.SimpleMDE && global.SimpleMDE.marked) {
|
||||
return global.SimpleMDE.marked;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getMarkdownTokens(markdownText, renderer) {
|
||||
var marked = getMarkedEngine(renderer);
|
||||
var options = createMarkdownOptions(renderer);
|
||||
var lexer = marked && (marked.lexer || (marked.Lexer && marked.Lexer.lex));
|
||||
if (typeof lexer !== "function") {
|
||||
return null;
|
||||
}
|
||||
var tokens = lexer(String(markdownText || ""), options);
|
||||
if (tokens) {
|
||||
tokens.marked = marked;
|
||||
tokens.options = options;
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
function getInlineRules(marked, options) {
|
||||
var inlineLexer = marked && marked.InlineLexer;
|
||||
if (!inlineLexer || !inlineLexer.rules) {
|
||||
return null;
|
||||
}
|
||||
if (options.gfm) {
|
||||
return options.breaks ? inlineLexer.rules.breaks : inlineLexer.rules.gfm;
|
||||
}
|
||||
if (options.pedantic) {
|
||||
return inlineLexer.rules.pedantic;
|
||||
}
|
||||
return inlineLexer.rules.normal;
|
||||
}
|
||||
|
||||
function createInlineContext(links, options, marked) {
|
||||
return {
|
||||
inLink: false,
|
||||
links: links || {},
|
||||
marked: marked,
|
||||
options: options || {},
|
||||
rules: getInlineRules(marked, options || {}),
|
||||
};
|
||||
}
|
||||
|
||||
function cloneInlineContext(context, inLink) {
|
||||
return {
|
||||
inLink: !!inLink,
|
||||
links: context.links,
|
||||
marked: context.marked,
|
||||
options: context.options,
|
||||
rules: context.rules,
|
||||
};
|
||||
}
|
||||
|
||||
function createSafeLink(href, title, content) {
|
||||
var safeHref = sanitizeUrl(href);
|
||||
if (!safeHref) {
|
||||
return content;
|
||||
}
|
||||
var link = document.createElement("a");
|
||||
link.setAttribute("href", safeHref);
|
||||
if (title) {
|
||||
link.setAttribute("title", title);
|
||||
}
|
||||
appendNode(link, content);
|
||||
return link;
|
||||
}
|
||||
|
||||
function createSafeImage(src, title, altText) {
|
||||
var safeSrc = sanitizeUrl(src, { allowDataImage: true });
|
||||
if (!safeSrc) {
|
||||
return document.createTextNode(altText || "");
|
||||
}
|
||||
var image = document.createElement("img");
|
||||
image.setAttribute("src", safeSrc);
|
||||
if (altText) {
|
||||
image.setAttribute("alt", altText);
|
||||
}
|
||||
if (title) {
|
||||
image.setAttribute("title", title);
|
||||
}
|
||||
return image;
|
||||
}
|
||||
|
||||
function createInlineElement(tagName, content) {
|
||||
var element = document.createElement(tagName);
|
||||
appendNode(element, content);
|
||||
return element;
|
||||
}
|
||||
|
||||
function createBlockElement(tagName, content) {
|
||||
var element = document.createElement(tagName);
|
||||
appendNode(element, content);
|
||||
return element;
|
||||
}
|
||||
|
||||
function createLiteralBlock(text) {
|
||||
var value = String(text || "");
|
||||
if (value.indexOf("\n") >= 0) {
|
||||
var pre = document.createElement("pre");
|
||||
pre.appendChild(document.createTextNode(value));
|
||||
return pre;
|
||||
}
|
||||
return createBlockElement("p", document.createTextNode(value));
|
||||
}
|
||||
|
||||
function inlineContextForTokens(tokens) {
|
||||
return createInlineContext(
|
||||
(tokens && tokens.links) || {},
|
||||
(tokens && tokens.options) || {},
|
||||
tokens && tokens.marked,
|
||||
);
|
||||
}
|
||||
|
||||
function collectTextTokens(firstToken, tokens, state) {
|
||||
var text = String((firstToken && firstToken.text) || "");
|
||||
while (
|
||||
state.index < tokens.length &&
|
||||
tokens[state.index] &&
|
||||
tokens[state.index].type === "text"
|
||||
) {
|
||||
text += "\n" + String(tokens[state.index].text || "");
|
||||
state.index += 1;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
function renderInline(text, context) {
|
||||
var source = String(text || "");
|
||||
var rules = context && context.rules;
|
||||
var fragment = emptyFragment();
|
||||
if (!rules) {
|
||||
appendDecodedText(fragment, source);
|
||||
return fragment;
|
||||
}
|
||||
|
||||
while (source) {
|
||||
var match;
|
||||
var label;
|
||||
var definition;
|
||||
if ((match = rules.escape.exec(source))) {
|
||||
source = source.substring(match[0].length);
|
||||
appendDecodedText(fragment, match[1]);
|
||||
continue;
|
||||
}
|
||||
if ((match = rules.autolink.exec(source))) {
|
||||
source = source.substring(match[0].length);
|
||||
if (match[2] === "@") {
|
||||
label =
|
||||
match[1].charAt(6) === ":"
|
||||
? match[1].substring(7)
|
||||
: match[1];
|
||||
appendNode(
|
||||
fragment,
|
||||
createSafeLink(
|
||||
"mailto:" + label,
|
||||
null,
|
||||
document.createTextNode(label),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
label = match[1];
|
||||
appendNode(
|
||||
fragment,
|
||||
createSafeLink(label, null, document.createTextNode(label)),
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!context.inLink && (match = rules.url.exec(source))) {
|
||||
source = source.substring(match[0].length);
|
||||
label = match[0];
|
||||
appendNode(
|
||||
fragment,
|
||||
createSafeLink(label, null, document.createTextNode(label)),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if ((match = rules.tag.exec(source))) {
|
||||
source = source.substring(match[0].length);
|
||||
appendLiteralText(fragment, match[0]);
|
||||
continue;
|
||||
}
|
||||
if ((match = rules.link.exec(source))) {
|
||||
source = source.substring(match[0].length);
|
||||
if (match[0].charAt(0) === "!") {
|
||||
appendNode(
|
||||
fragment,
|
||||
createSafeImage(match[2], match[3], decodeEntities(match[1] || "")),
|
||||
);
|
||||
} else {
|
||||
appendNode(
|
||||
fragment,
|
||||
createSafeLink(
|
||||
match[2],
|
||||
match[3],
|
||||
renderInline(match[1], cloneInlineContext(context, true)),
|
||||
),
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
match = rules.reflink.exec(source) || rules.nolink.exec(source);
|
||||
if (match) {
|
||||
source = source.substring(match[0].length);
|
||||
label = (match[2] || match[1] || "").replace(/\s+/g, " ");
|
||||
definition = context.links[String(label).toLowerCase()];
|
||||
if (!definition || !definition.href) {
|
||||
appendLiteralText(fragment, match[0].charAt(0));
|
||||
source = match[0].substring(1) + source;
|
||||
continue;
|
||||
}
|
||||
if (match[0].charAt(0) === "!") {
|
||||
appendNode(
|
||||
fragment,
|
||||
createSafeImage(
|
||||
definition.href,
|
||||
definition.title,
|
||||
decodeEntities(match[1] || ""),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
appendNode(
|
||||
fragment,
|
||||
createSafeLink(
|
||||
definition.href,
|
||||
definition.title,
|
||||
renderInline(match[1], cloneInlineContext(context, true)),
|
||||
),
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if ((match = rules.strong.exec(source))) {
|
||||
source = source.substring(match[0].length);
|
||||
appendNode(
|
||||
fragment,
|
||||
createInlineElement(
|
||||
"strong",
|
||||
renderInline(
|
||||
match[2] || match[1],
|
||||
cloneInlineContext(context, context.inLink),
|
||||
),
|
||||
),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if ((match = rules.em.exec(source))) {
|
||||
source = source.substring(match[0].length);
|
||||
appendNode(
|
||||
fragment,
|
||||
createInlineElement(
|
||||
"em",
|
||||
renderInline(
|
||||
match[2] || match[1],
|
||||
cloneInlineContext(context, context.inLink),
|
||||
),
|
||||
),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if ((match = rules.code.exec(source))) {
|
||||
source = source.substring(match[0].length);
|
||||
var inlineCode = document.createElement("code");
|
||||
inlineCode.textContent = match[2] || "";
|
||||
appendNode(fragment, inlineCode);
|
||||
continue;
|
||||
}
|
||||
if ((match = rules.br.exec(source))) {
|
||||
source = source.substring(match[0].length);
|
||||
appendNode(fragment, document.createElement("br"));
|
||||
continue;
|
||||
}
|
||||
if ((match = rules.del.exec(source))) {
|
||||
source = source.substring(match[0].length);
|
||||
appendNode(
|
||||
fragment,
|
||||
createInlineElement(
|
||||
"del",
|
||||
renderInline(
|
||||
match[1],
|
||||
cloneInlineContext(context, context.inLink),
|
||||
),
|
||||
),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if ((match = rules.text.exec(source))) {
|
||||
source = source.substring(match[0].length);
|
||||
appendDecodedText(fragment, match[0]);
|
||||
continue;
|
||||
}
|
||||
appendLiteralText(fragment, source.charAt(0));
|
||||
source = source.substring(1);
|
||||
}
|
||||
|
||||
return fragment;
|
||||
}
|
||||
|
||||
function renderTextParagraph(text, tokens) {
|
||||
return createBlockElement("p", renderInline(text, inlineContextForTokens(tokens)));
|
||||
}
|
||||
|
||||
function renderHeading(token, tokens) {
|
||||
var depth = parseInt(token.depth, 10);
|
||||
if (isNaN(depth) || depth < 1 || depth > 6) {
|
||||
depth = 1;
|
||||
}
|
||||
return createBlockElement(
|
||||
"h" + String(depth),
|
||||
renderInline(token.text, inlineContextForTokens(tokens)),
|
||||
);
|
||||
}
|
||||
|
||||
function renderCodeBlock(token) {
|
||||
var pre = document.createElement("pre");
|
||||
var code = document.createElement("code");
|
||||
if (token.lang) {
|
||||
var className = "lang-" + String(token.lang).trim();
|
||||
if (CODE_CLASS_RE.test(className)) {
|
||||
code.className = className;
|
||||
}
|
||||
}
|
||||
code.textContent = token.text || "";
|
||||
pre.appendChild(code);
|
||||
return pre;
|
||||
}
|
||||
|
||||
function renderTable(token, tokens) {
|
||||
var table = document.createElement("table");
|
||||
var thead = document.createElement("thead");
|
||||
var tbody = document.createElement("tbody");
|
||||
var row = document.createElement("tr");
|
||||
var inlineContext = inlineContextForTokens(tokens);
|
||||
var i;
|
||||
var j;
|
||||
for (i = 0; i < token.header.length; i++) {
|
||||
var th = document.createElement("th");
|
||||
appendNode(th, renderInline(token.header[i], cloneInlineContext(inlineContext, false)));
|
||||
applyTextAlign(th, token.align && token.align[i]);
|
||||
row.appendChild(th);
|
||||
}
|
||||
thead.appendChild(row);
|
||||
table.appendChild(thead);
|
||||
for (i = 0; i < token.cells.length; i++) {
|
||||
row = document.createElement("tr");
|
||||
for (j = 0; j < token.cells[i].length; j++) {
|
||||
var td = document.createElement("td");
|
||||
appendNode(td, renderInline(token.cells[i][j], cloneInlineContext(inlineContext, false)));
|
||||
applyTextAlign(td, token.align && token.align[j]);
|
||||
row.appendChild(td);
|
||||
}
|
||||
tbody.appendChild(row);
|
||||
}
|
||||
table.appendChild(tbody);
|
||||
return table;
|
||||
}
|
||||
|
||||
function renderListItem(loose, tokens, state) {
|
||||
var item = document.createElement("li");
|
||||
while (state.index < tokens.length) {
|
||||
var token = tokens[state.index++];
|
||||
if (!token || token.type === "list_item_end") {
|
||||
break;
|
||||
}
|
||||
if (token.type === "space") {
|
||||
continue;
|
||||
}
|
||||
if (!loose && token.type === "text") {
|
||||
appendNode(
|
||||
item,
|
||||
renderInline(
|
||||
collectTextTokens(token, tokens, state),
|
||||
inlineContextForTokens(tokens),
|
||||
),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
appendNode(item, renderBlockToken(token, tokens, state));
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
function renderList(token, tokens, state) {
|
||||
var list = document.createElement(token.ordered ? "ol" : "ul");
|
||||
while (state.index < tokens.length) {
|
||||
var itemToken = tokens[state.index++];
|
||||
if (!itemToken) {
|
||||
break;
|
||||
}
|
||||
if (itemToken.type === "list_end") {
|
||||
break;
|
||||
}
|
||||
if (
|
||||
itemToken.type !== "list_item_start" &&
|
||||
itemToken.type !== "loose_item_start"
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
list.appendChild(
|
||||
renderListItem(itemToken.type === "loose_item_start", tokens, state),
|
||||
);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
function renderBlockToken(token, tokens, state) {
|
||||
switch (token.type) {
|
||||
case "space":
|
||||
return emptyFragment();
|
||||
case "hr":
|
||||
return document.createElement("hr");
|
||||
case "heading":
|
||||
return renderHeading(token, tokens);
|
||||
case "code":
|
||||
return renderCodeBlock(token);
|
||||
case "table":
|
||||
return renderTable(token, tokens);
|
||||
case "blockquote_start":
|
||||
return createBlockElement(
|
||||
"blockquote",
|
||||
renderBlocks(tokens, state, "blockquote_end"),
|
||||
);
|
||||
case "list_start":
|
||||
return renderList(token, tokens, state);
|
||||
case "html":
|
||||
return createLiteralBlock(token.text);
|
||||
case "paragraph":
|
||||
return renderTextParagraph(token.text, tokens);
|
||||
case "text":
|
||||
return renderTextParagraph(collectTextTokens(token, tokens, state), tokens);
|
||||
default:
|
||||
return createLiteralBlock(token.text || "");
|
||||
}
|
||||
}
|
||||
|
||||
function renderBlocks(tokens, state, stopType) {
|
||||
var fragment = emptyFragment();
|
||||
while (state.index < tokens.length) {
|
||||
var token = tokens[state.index++];
|
||||
if (!token) {
|
||||
break;
|
||||
}
|
||||
if (stopType && token.type === stopType) {
|
||||
break;
|
||||
}
|
||||
appendNode(fragment, renderBlockToken(token, tokens, state));
|
||||
}
|
||||
return fragment;
|
||||
}
|
||||
|
||||
function buildMarkdownFragment(markdownText, renderer) {
|
||||
var tokens = getMarkdownTokens(markdownText, renderer);
|
||||
if (!tokens) {
|
||||
var fallback = document.createElement("pre");
|
||||
fallback.textContent = String(markdownText || "");
|
||||
var fragment = emptyFragment();
|
||||
fragment.appendChild(fallback);
|
||||
return fragment;
|
||||
}
|
||||
return renderBlocks(tokens, { index: 0 });
|
||||
}
|
||||
|
||||
global.NotebookSafeMarkdown = {
|
||||
buildFragment: function (markdownText, renderer) {
|
||||
return buildMarkdownFragment(markdownText, renderer);
|
||||
},
|
||||
renderInto: function (container, markdownText, renderer) {
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
container.replaceChildren(buildMarkdownFragment(markdownText, renderer));
|
||||
},
|
||||
};
|
||||
|
||||
global.NotebookShortcuts = {
|
||||
bind: bind,
|
||||
annotate: annotate,
|
||||
combo: combo,
|
||||
formatCombo: formatCombo,
|
||||
record: record,
|
||||
isEditableTarget: isEditableTarget,
|
||||
defaults: defaults,
|
||||
};
|
||||
})(window, document);
|
||||
@@ -11,5 +11,5 @@ if(!/^s(hift)$/i.test(l))throw new Error("Unrecognized modifier name: "+l);r=!0}
|
||||
},receivedFocus:function(){this.slowPoll()},slowPoll:function(){var e=this;e.pollingFast||e.polling.set(this.cm.options.pollInterval,function(){e.poll(),e.cm.state.focused&&e.slowPoll()})},fastPoll:function(){function e(){var r=n.poll();r||t?(n.pollingFast=!1,n.slowPoll()):(t=!0,n.polling.set(60,e))}var t=!1,n=this;n.pollingFast=!0,n.polling.set(20,e)},poll:function(){var e=this.cm,t=this.textarea,n=this.prevInput;if(this.contextMenuPending||!e.state.focused||nl(t)&&!n&&!this.composing||e.isReadOnly()||e.options.disableInput||e.state.keySeq)return!1;var r=t.value;if(r==n&&!e.somethingSelected())return!1;if(xo&&bo>=9&&this.hasSelection===r||Eo&&/[\uf700-\uf7ff]/.test(r))return e.display.input.reset(),!1;if(e.doc.sel==e.display.selForContextMenu){var i=r.charCodeAt(0);if(8203!=i||n||(n=""),8666==i)return this.reset(),this.cm.execCommand("undo")}for(var o=0,a=Math.min(n.length,r.length);a>o&&n.charCodeAt(o)==r.charCodeAt(o);)++o;var l=this;return At(e,function(){Z(e,r.slice(o),n.length-o,null,l.composing?"*compose":null),r.length>1e3||r.indexOf("\n")>-1?t.value=l.prevInput="":l.prevInput=r,l.composing&&(l.composing.range.clear(),l.composing.range=e.markText(l.composing.start,e.getCursor("to"),{className:"CodeMirror-composing"}))}),!0},ensurePolled:function(){this.pollingFast&&this.poll()&&(this.pollingFast=!1)},onKeyPress:function(){xo&&bo>=9&&(this.hasSelection=null),this.fastPoll()},onContextMenu:function(e){function t(){if(null!=a.selectionStart){var e=i.somethingSelected(),t=""+(e?a.value:"");a.value="⇚",a.value=t,r.prevInput=e?"":"",a.selectionStart=1,a.selectionEnd=t.length,o.selForContextMenu=i.doc.sel}}function n(){if(r.contextMenuPending=!1,r.wrapper.style.cssText=f,a.style.cssText=u,xo&&9>bo&&o.scrollbars.setScrollTop(o.scroller.scrollTop=s),null!=a.selectionStart){(!xo||xo&&9>bo)&&t();var e=0,n=function(){o.selForContextMenu==i.doc.sel&&0==a.selectionStart&&a.selectionEnd>0&&""==r.prevInput?Et(i,ua.selectAll)(i):e++<10?o.detectingSelectAll=setTimeout(n,500):o.input.reset()};o.detectingSelectAll=setTimeout(n,200)}}var r=this,i=r.cm,o=i.display,a=r.textarea,l=Yt(i,e),s=o.scroller.scrollTop;if(l&&!Co){var c=i.options.resetSelectionOnContextMenu;c&&-1==i.doc.sel.contains(l)&&Et(i,Te)(i.doc,de(l),Wa);var u=a.style.cssText,f=r.wrapper.style.cssText;r.wrapper.style.cssText="position: absolute";var h=r.wrapper.getBoundingClientRect();if(a.style.cssText="position: absolute; width: 30px; height: 30px; top: "+(e.clientY-h.top-5)+"px; left: "+(e.clientX-h.left-5)+"px; z-index: 1000; background: "+(xo?"rgba(255, 255, 255, .05)":"transparent")+"; outline: none; border-width: 0; outline: none; overflow: hidden; opacity: .05; filter: alpha(opacity=5);",wo)var d=window.scrollY;if(o.input.focus(),wo&&window.scrollTo(null,d),o.input.reset(),i.somethingSelected()||(a.value=r.prevInput=" "),r.contextMenuPending=!0,o.selForContextMenu=i.doc.sel,clearTimeout(o.detectingSelectAll),xo&&bo>=9&&t(),Do){Aa(e);var p=function(){Ia(window,"mouseup",p),setTimeout(n,20)};Ea(window,"mouseup",p)}else setTimeout(n,50)}},readOnlyChanged:function(e){e||this.reset()},setUneditable:Di,needsContentAttribute:!1},ne.prototype),ie.prototype=Wi({init:function(e){function t(e){if(!Ti(r,e)){if(r.somethingSelected())Fo={lineWise:!1,text:r.getSelections()},"cut"==e.type&&r.replaceSelection("",null,"cut");else{if(!r.options.lineWiseCopyCut)return;var t=ee(r);Fo={lineWise:!0,text:t.text},"cut"==e.type&&r.operation(function(){r.setSelections(t.ranges,0,Wa),r.replaceSelection("",null,"cut")})}if(e.clipboardData&&!No)e.preventDefault(),e.clipboardData.clearData(),e.clipboardData.setData("text/plain",Fo.text.join("\n"));else{var n=re(),i=n.firstChild;r.display.lineSpace.insertBefore(n,r.display.lineSpace.firstChild),i.value=Fo.text.join("\n");var o=document.activeElement;Ua(i),setTimeout(function(){r.display.lineSpace.removeChild(n),o.focus()},50)}}}var n=this,r=n.cm,i=n.div=e.lineDiv;te(i),Ea(i,"paste",function(e){Ti(r,e)||J(e,r)}),Ea(i,"compositionstart",function(e){var t=e.data;if(n.composing={sel:r.doc.sel,data:t,startData:t},t){var i=r.doc.sel.primary(),o=r.getLine(i.head.line),a=o.indexOf(t,Math.max(0,i.head.ch-t.length));a>-1&&a<=i.head.ch&&(n.composing.sel=de(Bo(i.head.line,a),Bo(i.head.line,a+t.length)))}}),Ea(i,"compositionupdate",function(e){n.composing.data=e.data}),Ea(i,"compositionend",function(e){var t=n.composing;t&&(e.data==t.startData||/\u200b/.test(e.data)||(t.data=e.data),setTimeout(function(){t.handled||n.applyComposition(t),n.composing==t&&(n.composing=null)},50))}),Ea(i,"touchstart",function(){n.forceCompositionEnd()}),Ea(i,"input",function(){n.composing||!r.isReadOnly()&&n.pollContent()||At(n.cm,function(){Dt(r)})}),Ea(i,"copy",t),Ea(i,"cut",t)},prepareSelection:function(){var e=De(this.cm,!1);return e.focus=this.cm.state.focused,e},showSelection:function(e,t){e&&this.cm.display.view.length&&((e.focus||t)&&this.showPrimarySelection(),this.showMultipleSelections(e))},showPrimarySelection:function(){var e=window.getSLine truncated
|
||||
if(a)return a}}},da=e.isModifierKey=function(e){var t="string"==typeof e?e:ol[e.keyCode];return"Ctrl"==t||"Alt"==t||"Shift"==t||"Mod"==t},pa=e.keyName=function(e,t){if(Co&&34==e.keyCode&&e["char"])return!1;var n=ol[e.keyCode],r=n;return null==r||e.altGraphKey?!1:(e.altKey&&"Alt"!=n&&(r="Alt-"+r),(Ro?e.metaKey:e.ctrlKey)&&"Ctrl"!=n&&(r="Ctrl-"+r),(Ro?e.ctrlKey:e.metaKey)&&"Cmd"!=n&&(r="Cmd-"+r),!t&&e.shiftKey&&"Shift"!=n&&(r="Shift-"+r),r)};e.fromTextArea=function(t,n){function r(){t.value=c.getValue()}if(n=n?Wi(n):{},n.value=t.value,!n.tabindex&&t.tabIndex&&(n.tabindex=t.tabIndex),!n.placeholder&&t.placeholder&&(n.placeholder=t.placeholder),null==n.autofocus){var i=Gi();n.autofocus=i==t||null!=t.getAttribute("autofocus")&&i==document.body}if(t.form&&(Ea(t.form,"submit",r),!n.leaveSubmitMethodAlone)){var o=t.form,a=o.submit;try{var l=o.submit=function(){r(),o.submit=a,o.submit(),o.submit=l}}catch(s){}}n.finishInit=function(e){e.save=r,e.getTextArea=function(){return t},e.toTextArea=function(){e.toTextArea=isNaN,r(),t.parentNode.removeChild(e.getWrapperElement()),t.style.display="",t.form&&(Ia(t.form,"submit",r),"function"==typeof t.form.submit&&(t.form.submit=a))}},t.style.display="none";var c=e(function(e){t.parentNode.insertBefore(e,t.nextSibling)},n);return c};var ma=e.StringStream=function(e,t){this.pos=this.start=0,this.string=e,this.tabSize=t||8,this.lastColumnPos=this.lastColumnValue=0,this.lineStart=0};ma.prototype={eol:function(){return this.pos>=this.string.length},sol:function(){return this.pos==this.lineStart},peek:function(){return this.string.charAt(this.pos)||void 0},next:function(){return this.pos<this.string.length?this.string.charAt(this.pos++):void 0},eat:function(e){var t=this.string.charAt(this.pos);if("string"==typeof e)var n=t==e;else var n=t&&(e.test?e.test(t):e(t));return n?(++this.pos,t):void 0},eatWhile:function(e){for(var t=this.pos;this.eat(e););return this.pos>t},eatSpace:function(){for(var e=this.pos;/[\s\u00a0]/.test(this.string.charAt(this.pos));)++this.pos;return this.pos>e},skipToEnd:function(){this.pos=this.string.length},skipTo:function(e){var t=this.string.indexOf(e,this.pos);return t>-1?(this.pos=t,!0):void 0},backUp:function(e){this.pos-=e},column:function(){return this.lastColumnPos<this.start&&(this.lastColumnValue=Fa(this.string,this.start,this.tabSize,this.lastColumnPos,this.lastColumnValue),this.lastColumnPos=this.start),this.lastColumnValue-(this.lineStart?Fa(this.string,this.lineStart,this.tabSize):0)},indentation:function(){return Fa(this.string,null,this.tabSize)-(this.lineStart?Fa(this.string,this.lineStart,this.tabSize):0)},match:function(e,t,n){if("string"!=typeof e){var r=this.string.slice(this.pos).match(e);return r&&r.index>0?null:(r&&t!==!1&&(this.pos+=r[0].length),r)}var i=function(e){return n?e.toLowerCase():e},o=this.string.substr(this.pos,e.length);return i(o)==i(e)?(t!==!1&&(this.pos+=e.length),!0):void 0},current:function(){return this.string.slice(this.start,this.pos)},hideFirstChars:function(e,t){this.lineStart+=e;try{return t()}finally{this.lineStart-=e}}};var ga=0,va=e.TextMarker=function(e,t){this.lines=[],this.type=t,this.doc=e,this.id=++ga};Ai(va),va.prototype.clear=function(){if(!this.explicitlyCleared){var e=this.doc.cm,t=e&&!e.curOp;if(t&&bt(e),Ni(this,"clear")){var n=this.find();n&&Ci(this,"clear",n.from,n.to)}for(var r=null,i=null,o=0;o<this.lines.length;++o){var a=this.lines[o],l=er(a.markedSpans,this);e&&!this.collapsed?Ht(e,ti(a),"text"):e&&(null!=l.to&&(i=ti(a)),null!=l.from&&(r=ti(a))),a.markedSpans=tr(a.markedSpans,l),null==l.from&&this.collapsed&&!kr(this.doc,a)&&e&&ei(a,yt(e.display))}if(e&&this.collapsed&&!e.options.lineWrapping)for(var o=0;o<this.lines.length;++o){var s=yr(this.lines[o]),c=f(s);c>e.display.maxLineLength&&(e.display.maxLine=s,e.display.maxLineLength=c,e.display.maxLineChanged=!0)}null!=r&&e&&this.collapsed&&Dt(e,r,i+1),this.lines.length=0,this.explicitlyCleared=!0,this.atomic&&this.doc.cantEdit&&(this.doc.cantEdit=!1,e&&Ae(e.doc)),e&&Ci(e,"markerCleared",e,this),t&&kt(e),this.parent&&this.parent.clear()}},va.prototype.find=function(e,t){null==e&&"bookmark"==this.type&&(e=1);for(var n,r,i=0;i<this.lines.length;++i){var o=this.lines[i],a=er(o.markedSpans,this);if(null!=a.from&&(n=Bo(t?o:ti(o),a.from),-1==e))return n;if(null!=a.to&&(r=Bo(t?o:ti(o),a.to),1==e))return r}return n&&{from:n,to:r}},va.prototype.changed=function(){var e=this.find(-1,!0),t=this,n=this.doc.cm;e&&n&&At(n,function(){var r=e.line,i=ti(e.line),o=Qe(n,i);if(o&&(ot(o),n.curOp.selectionChanged=n.curOp.forceUpdate=!0),n.curOp.updateMaxLine=!0,!kr(t.doc,r)&&null!=t.height){var a=t.height;t.height=null;var l=Lr(t)-a;l&&ei(r,r.height+l)}})},va.prototype.attachLine=function(e){if(!this.lines.length&&this.doc.cm){var t=this.doc.cm.curOp;t.maybeHiddenMarkers&&-1!=Pi(t.maybeHiddenMarkers,this)||(t.maybeUnhiddenMarkers||(t.maybeUnhiddenMarkers=[])).push(this)}this.lines.push(e)},va.prototype.detachLine=function(e){if(this.lines.splice(Pi(this.lines,e),1Line truncated
|
||||
h(r);var l=t.sol(),s=t.next();if(r.linkTitle){r.linkTitle=!1;var u=s;"("===s&&(u=")"),u=(u+"").replace(/([.?*+^$[\]\\(){}|-])/g,"\\$1");var f="^\\s*(?:[^"+u+"\\\\]+|\\\\\\\\|\\\\.)"+u;if(t.match(new RegExp(f),!0))return S.linkHref}if("`"===s){var d=r.formatting;n.highlightFormatting&&(r.formatting="code"),t.eatWhile("`");var p=t.current().length;if(0==r.code)return r.code=p,h(r);if(p==r.code){var v=h(r);return r.code=0,v}return r.formatting=d,h(r)}if(r.code)return h(r);if("\\"===s&&(t.next(),n.highlightFormatting)){var y=h(r),x=S.formatting+"-escape";return y?y+" "+x:x}if("!"===s&&t.match(/\[[^\]]*\] ?(?:\(|\[)/,!1))return t.match(/\[[^\]]*\]/),r.inline=r.f=g,S.image;if("["===s&&t.match(/[^\]]*\](\(.*\)| ?\[.*?\])/,!1))return r.linkText=!0,n.highlightFormatting&&(r.formatting="link"),h(r);if("]"===s&&r.linkText&&t.match(/\(.*?\)| ?\[.*?\]/,!1)){n.highlightFormatting&&(r.formatting="link");var y=h(r);return r.linkText=!1,r.inline=r.f=g,y}if("<"===s&&t.match(/^(https?|ftps?):\/\/(?:[^\\>]|\\.)+>/,!1)){r.f=r.inline=m,n.highlightFormatting&&(r.formatting="link");var y=h(r);return y?y+=" ":y="",y+S.linkInline}if("<"===s&&t.match(/^[^> \\]+@(?:[^\\>]|\\.)+>/,!1)){r.f=r.inline=m,n.highlightFormatting&&(r.formatting="link");var y=h(r);return y?y+=" ":y="",y+S.linkEmail}if("<"===s&&t.match(/^(!--|\w)/,!1)){var b=t.string.indexOf(">",t.pos);if(-1!=b){var k=t.string.substring(t.start,b);/markdown\s*=\s*('|"){0,1}1('|"){0,1}/.test(k)&&(r.md_inside=!0)}return t.backUp(1),r.htmlState=e.startState(w),o(t,r,c)}if("<"===s&&t.match(/^\/\w*?>/))return r.md_inside=!1,"tag";var C=!1;if(!n.underscoresBreakWords&&"_"===s&&"_"!==t.peek()&&t.match(/(\w)/,!1)){var L=t.pos-2;if(L>=0){var T=t.string.charAt(L);"_"!==T&&T.match(/(\w)/,!1)&&(C=!0)}}if("*"===s||"_"===s&&!C)if(l&&" "===t.peek());else{if(r.strong===s&&t.eat(s)){n.highlightFormatting&&(r.formatting="strong");var v=h(r);return r.strong=!1,v}if(!r.strong&&t.eat(s))return r.strong=s,n.highlightFormatting&&(r.formatting="strong"),h(r);if(r.em===s){n.highlightFormatting&&(r.formatting="em");var v=h(r);return r.em=!1,v}if(!r.em)return r.em=s,n.highlightFormatting&&(r.formatting="em"),h(r)}else if(" "===s&&(t.eat("*")||t.eat("_"))){if(" "===t.peek())return h(r);t.backUp(1)}if(n.strikethrough)if("~"===s&&t.eatWhile(s)){if(r.strikethrough){n.highlightFormatting&&(r.formatting="strikethrough");var v=h(r);return r.strikethrough=!1,v}if(t.match(/^[^\s]/,!1))return r.strikethrough=!0,n.highlightFormatting&&(r.formatting="strikethrough"),h(r)}else if(" "===s&&t.match(/^~~/,!0)){if(" "===t.peek())return h(r);t.backUp(2)}return" "===s&&(t.match(/ +$/,!1)?r.trailingSpace++:r.trailingSpace&&(r.trailingSpaceNewLine=!0)),h(r)}function m(e,t){var r=e.next();if(">"===r){t.f=t.inline=p,n.highlightFormatting&&(t.formatting="link");var i=h(t);return i?i+=" ":i="",i+S.linkInline}return e.match(/^[^>]+/,!0),S.linkInline}function g(e,t){if(e.eatSpace())return null;var r=e.next();return"("===r||"["===r?(t.f=t.inline=v("("===r?")":"]",0),n.highlightFormatting&&(t.formatting="link-string"),t.linkHref=!0,h(t)):"error"}function v(e){return function(t,r){var i=t.next();if(i===e){r.f=r.inline=p,n.highlightFormatting&&(r.formatting="link-string");var o=h(r);return r.linkHref=!1,o}return t.match(P[e]),r.linkHref=!0,h(r)}}function y(e,t){return e.match(/^([^\]\\]|\\.)*\]:/,!1)?(t.f=x,e.next(),n.highlightFormatting&&(t.formatting="link"),t.linkText=!0,h(t)):i(e,t,p)}function x(e,t){if(e.match(/^\]:/,!0)){t.f=t.inline=b,n.highlightFormatting&&(t.formatting="link");var r=h(t);return t.linkText=!1,r}return e.match(/^([^\]\\]|\\.)+/,!0),S.linkText}function b(e,t){return e.eatSpace()?null:(e.match(/^[^\s]+/,!0),void 0===e.peek()?t.linkTitle=!0:e.match(/^(?:\s+(?:"(?:[^"\\]|\\\\|\\.)+"|'(?:[^'\\]|\\\\|\\.)+'|\((?:[^)\\]|\\\\|\\.)+\)))?/,!0),t.f=t.inline=p,S.linkHref+" url")}var w=e.getMode(t,"text/html"),k="null"==w.name;void 0===n.highlightFormatting&&(n.highlightFormatting=!1),void 0===n.maxBlockquoteDepth&&(n.maxBlockquoteDepth=0),void 0===n.underscoresBreakWords&&(n.underscoresBreakWords=!0),void 0===n.taskLists&&(n.taskLists=!1),void 0===n.strikethrough&&(n.strikethrough=!1),void 0===n.tokenTypeOverrides&&(n.tokenTypeOverrides={});var S={header:"header",code:"comment",quote:"quote",list1:"variable-2",list2:"variable-3",list3:"keyword",hr:"hr",image:"tag",formatting:"formatting",linkInline:"link",linkEmail:"link",linkText:"link",linkHref:"string",em:"em",strong:"strong",strikethrough:"strikethrough"};for(var C in S)S.hasOwnProperty(C)&&n.tokenTypeOverrides[C]&&(S[C]=n.tokenTypeOverrides[C]);var L=/^([*\-_])(?:\s*\1){2,}\s*$/,T=/^[*\-+]\s+/,M=/^[0-9]+([.)])\s+/,N=/^\[(x| )\](?=\s)/,A=n.allowAtxHeaderWithoutSpace?/^(#+)/:/^(#+)(?: |$)/,E=/^ *(?:\={1,}|-{1,})\s*$/,O=/^[^#!\[\]*_\\<>` "'(~]+/,I=new RegExp("^("+(n.fencedCodeBlocks===!0?"~~~+|```+":n.fencedCodeBlocks)+")[ \\t]*([\\w+#-]*)"),P={")":/^(?:[^\\\(\)]|\\.|\((?:[^\\\(\)]|\\.)*\))*?(?=\))/,"]":/^(?:[^\\\[\]]|\\.|\[(?:[^\\\[\\]]|\\.)*\])*?(?=\])/},R={startState:Line truncated
|
||||
header:o[1].replace(/^ *| *\| *$/g,"").split(/ *\| */),align:o[2].replace(/^ *|\| *$/g,"").split(/ *\| */),cells:o[3].replace(/(?: *\| *)?\n$/,"").split("\n")},u=0;u<s.align.length;u++)/^ *-+: *$/.test(s.align[u])?s.align[u]="right":/^ *:-+: *$/.test(s.align[u])?s.align[u]="center":/^ *:-+ *$/.test(s.align[u])?s.align[u]="left":s.align[u]=null;for(u=0;u<s.cells.length;u++)s.cells[u]=s.cells[u].replace(/^ *\| *| *\| *$/g,"").split(/ *\| */);this.tokens.push(s)}else if(t&&(o=this.rules.paragraph.exec(e)))e=e.substring(o[0].length),this.tokens.push({type:"paragraph",text:"\n"===o[1].charAt(o[1].length-1)?o[1].slice(0,-1):o[1]});else if(o=this.rules.text.exec(e))e=e.substring(o[0].length),this.tokens.push({type:"text",text:o[0]});else if(e)throw new Error("Infinite loop on byte: "+e.charCodeAt(0));return this.tokens};var p={escape:/^\\([\\`*{}\[\]()#+\-.!_>])/,autolink:/^<([^ >]+(@|:\/)[^ >]+)>/,url:u,tag:/^<!--[\s\S]*?-->|^<\/?\w+(?:"[^"]*"|'[^']*'|[^'">])*?>/,link:/^!?\[(inside)\]\(href\)/,reflink:/^!?\[(inside)\]\s*\[([^\]]*)\]/,nolink:/^!?\[((?:\[[^\]]*\]|[^\[\]])*)\]/,strong:/^__([\s\S]+?)__(?!_)|^\*\*([\s\S]+?)\*\*(?!\*)/,em:/^\b_((?:[^_]|__)+?)_\b|^\*((?:\*\*|[\s\S])+?)\*(?!\*)/,code:/^(`+)\s*([\s\S]*?[^`])\s*\1(?!`)/,br:/^ {2,}\n(?!\s*$)/,del:u,text:/^[\s\S]+?(?=[\\<!\[_*`]| {2,}\n|$)/};p._inside=/(?:\[[^\]]*\]|[^\[\]]|\](?=[^\[]*\]))*/,p._href=/\s*<?([\s\S]*?)>?(?:\s+['"]([\s\S]*?)['"])?\s*/,p.link=c(p.link)("inside",p._inside)("href",p._href)(),p.reflink=c(p.reflink)("inside",p._inside)(),p.normal=f({},p),p.pedantic=f({},p.normal,{strong:/^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/,em:/^_(?=\S)([\s\S]*?\S)_(?!_)|^\*(?=\S)([\s\S]*?\S)\*(?!\*)/}),p.gfm=f({},p.normal,{escape:c(p.escape)("])","~|])")(),url:/^(https?:\/\/[^\s<]+[^<.,:;"')\]\s])/,del:/^~~(?=\S)([\s\S]*?\S)~~/,text:c(p.text)("]|","~]|")("|","|https?://|")()}),p.breaks=f({},p.gfm,{br:c(p.br)("{2,}","*")(),text:c(p.gfm.text)("{2,}","*")()}),i.rules=p,i.output=function(e,t,n){var r=new i(t,n);return r.output(e)},i.prototype.output=function(e){for(var t,n,r,i,o="";e;)if(i=this.rules.escape.exec(e))e=e.substring(i[0].length),o+=i[1];else if(i=this.rules.autolink.exec(e))e=e.substring(i[0].length),"@"===i[2]?(n=":"===i[1].charAt(6)?this.mangle(i[1].substring(7)):this.mangle(i[1]),r=this.mangle("mailto:")+n):(n=l(i[1]),r=n),o+=this.renderer.link(r,null,n);else if(this.inLink||!(i=this.rules.url.exec(e))){if(i=this.rules.tag.exec(e))!this.inLink&&/^<a /i.test(i[0])?this.inLink=!0:this.inLink&&/^<\/a>/i.test(i[0])&&(this.inLink=!1),e=e.substring(i[0].length),o+=this.options.sanitize?this.options.sanitizer?this.options.sanitizer(i[0]):l(i[0]):i[0];else if(i=this.rules.link.exec(e))e=e.substring(i[0].length),this.inLink=!0,o+=this.outputLink(i,{href:i[2],title:i[3]}),this.inLink=!1;else if((i=this.rules.reflink.exec(e))||(i=this.rules.nolink.exec(e))){if(e=e.substring(i[0].length),t=(i[2]||i[1]).replace(/\s+/g," "),t=this.links[t.toLowerCase()],!t||!t.href){o+=i[0].charAt(0),e=i[0].substring(1)+e;continue}this.inLink=!0,o+=this.outputLink(i,t),this.inLink=!1}else if(i=this.rules.strong.exec(e))e=e.substring(i[0].length),o+=this.renderer.strong(this.output(i[2]||i[1]));else if(i=this.rules.em.exec(e))e=e.substring(i[0].length),o+=this.renderer.em(this.output(i[2]||i[1]));else if(i=this.rules.code.exec(e))e=e.substring(i[0].length),o+=this.renderer.codespan(l(i[2],!0));else if(i=this.rules.br.exec(e))e=e.substring(i[0].length),o+=this.renderer.br();else if(i=this.rules.del.exec(e))e=e.substring(i[0].length),o+=this.renderer.del(this.output(i[1]));else if(i=this.rules.text.exec(e))e=e.substring(i[0].length),o+=this.renderer.text(l(this.smartypants(i[0])));else if(e)throw new Error("Infinite loop on byte: "+e.charCodeAt(0))}else e=e.substring(i[0].length),n=l(i[1]),r=n,o+=this.renderer.link(r,null,n);return o},i.prototype.outputLink=function(e,t){var n=l(t.href),r=t.title?l(t.title):null;return"!"!==e[0].charAt(0)?this.renderer.link(n,r,this.output(e[1])):this.renderer.image(n,r,l(e[1]))},i.prototype.smartypants=function(e){return this.options.smartypants?e.replace(/---/g,"—").replace(/--/g,"–").replace(/(^|[-\u2014\/(\[{"\s])'/g,"$1‘").replace(/'/g,"’").replace(/(^|[-\u2014\/(\[{\u2018\s])"/g,"$1“").replace(/"/g,"”").replace(/\.{3}/g,"…"):e},i.prototype.mangle=function(e){if(!this.options.mangle)return e;for(var t,n="",r=e.length,i=0;r>i;i++)t=e.charCodeAt(i),Math.random()>.5&&(t="x"+t.toString(16)),n+="&#"+t+";";return n},o.prototype.code=function(e,t,n){if(this.options.highlight){var r=this.options.highlight(e,t);null!=r&&r!==e&&(n=!0,e=r)}return t?'<pre><code class="'+this.options.langPrefix+l(t,!0)+'">'+(n?e:l(e,!0))+"\n</code></pre>\n":"<pre><code>"+(n?e:l(e,!0))+"\n</code></pre>"},o.prototype.blockquote=function(e){return"<blockquote>\n"+e+"</blockquote>\n"},o.prototype.html=function(e){return e},o.prototype.heading=function(e,t,n){return"<h"+t+' id="'+this.options.headerPrefix+n.toLowerCase().replace(/[^\w]+/g,"-"Line truncated
|
||||
}(navigator.userAgent||navigator.vendor||window.opera),e},V="",K={bold:{name:"bold",action:c,className:"fa fa-bold",title:"Bold","default":!0},italic:{name:"italic",action:u,className:"fa fa-italic",title:"Italic","default":!0},strikethrough:{name:"strikethrough",action:f,className:"fa fa-strikethrough",title:"Strikethrough"},heading:{name:"heading",action:p,className:"fa fa-header",title:"Heading","default":!0},"heading-smaller":{name:"heading-smaller",action:p,className:"fa fa-header fa-header-x fa-header-smaller",title:"Smaller Heading"},"heading-bigger":{name:"heading-bigger",action:m,className:"fa fa-header fa-header-x fa-header-bigger",title:"Bigger Heading"},"heading-1":{name:"heading-1",action:g,className:"fa fa-header fa-header-x fa-header-1",title:"Big Heading"},"heading-2":{name:"heading-2",action:v,className:"fa fa-header fa-header-x fa-header-2",title:"Medium Heading"},"heading-3":{name:"heading-3",action:y,className:"fa fa-header fa-header-x fa-header-3",title:"Small Heading"},"separator-1":{name:"separator-1"},code:{name:"code",action:h,className:"fa fa-code",title:"Code"},quote:{name:"quote",action:d,className:"fa fa-quote-left",title:"Quote","default":!0},"unordered-list":{name:"unordered-list",action:x,className:"fa fa-list-ul",title:"Generic List","default":!0},"ordered-list":{name:"ordered-list",action:b,className:"fa fa-list-ol",title:"Numbered List","default":!0},"clean-block":{name:"clean-block",action:w,className:"fa fa-eraser fa-clean-block",title:"Clean block"},"separator-2":{name:"separator-2"},link:{name:"link",action:k,className:"fa fa-link",title:"Create Link","default":!0},image:{name:"image",action:S,className:"fa fa-picture-o",title:"Insert Image","default":!0},table:{name:"table",action:C,className:"fa fa-table",title:"Insert Table"},"horizontal-rule":{name:"horizontal-rule",action:L,className:"fa fa-minus",title:"Insert Horizontal Line"},"separator-3":{name:"separator-3"},preview:{name:"preview",action:A,className:"fa fa-eye no-disable",title:"Toggle Preview","default":!0},"side-by-side":{name:"side-by-side",action:N,className:"fa fa-columns no-disable no-mobile",title:"Toggle Side by Side","default":!0},fullscreen:{name:"fullscreen",action:s,className:"fa fa-arrows-alt no-disable no-mobile",title:"Toggle Fullscreen","default":!0},"separator-4":{name:"separator-4"},guide:{name:"guide",action:"https://simplemde.com/markdown-guide",className:"fa fa-question-circle",title:"Markdown Guide","default":!0},"separator-5":{name:"separator-5"},undo:{name:"undo",action:T,className:"fa fa-undo no-disable",title:"Undo"},redo:{name:"redo",action:M,className:"fa fa-repeat no-disable",title:"Redo"}},X={link:["[","](#url#)"],image:[""],table:["","\n\n| Column 1 | Column 2 | Column 3 |\n| -------- | -------- | -------- |\n| Text | Text | Text |\n\n"],horizontalRule:["","\n\n-----\n\n"]},Z={link:"URL for the link:",image:"URL of the image:"},J={bold:"**",code:"```",italic:"*"};B.prototype.markdown=function(e){if(j){var t={};return this.options&&this.options.renderingConfig&&this.options.renderingConfig.singleLineBreaks===!1?t.breaks=!1:t.breaks=!0,this.options&&this.options.renderingConfig&&this.options.renderingConfig.codeSyntaxHighlighting===!0&&window.hljs&&(t.highlight=function(e){return window.hljs.highlightAuto(e).value}),j.setOptions(t),j(e)}},B.prototype.render=function(e){if(e||(e=this.element||document.getElementsByTagName("textarea")[0]),!this._rendered||this._rendered!==e){this.element=e;var t=this.options,n=this,i={};for(var o in t.shortcuts)null!==t.shortcuts[o]&&null!==q[o]&&!function(e){i[r(t.shortcuts[e])]=function(){q[e](n)}}(o);i.Enter="newlineAndIndentContinueMarkdownList",i.Tab="tabAndIndentMarkdownList",i["Shift-Tab"]="shiftTabAndUnindentMarkdownList",i.Esc=function(e){e.getOption("fullScreen")&&s(n)},document.addEventListener("keydown",function(e){e=e||window.event,27==e.keyCode&&n.codemirror.getOption("fullScreen")&&s(n)},!1);var a,l;if(t.spellChecker!==!1?(a="spell-checker",l=t.parsingConfig,l.name="gfm",l.gitHubSpice=!1,z({codeMirrorInstance:F})):(a=t.parsingConfig,a.name="gfm",a.gitHubSpice=!1),this.codemirror=F.fromTextArea(e,{mode:a,backdrop:l,theme:"paper",tabSize:void 0!=t.tabSize?t.tabSize:2,indentUnit:void 0!=t.tabSize?t.tabSize:2,indentWithTabs:t.indentWithTabs!==!1,lineNumbers:!1,autofocus:t.autofocus===!0,extraKeys:i,lineWrapping:t.lineWrapping!==!1,allowDropFileTypes:["text/plain"],placeholder:t.placeholder||e.getAttribute("placeholder")||"",styleSelectedText:void 0!=t.styleSelectedText?t.styleSelectedText:!0}),t.forceSync===!0){var c=this.codemirror;c.on("change",function(){c.save()})}this.gui={},t.toolbar!==!1&&(this.gui.toolbar=this.createToolbar()),t.status!==!1&&(this.gui.statusbar=this.createStatusbar()),void 0!=t.autosave&&t.autosave.enabled===!0&&this.autosave(),this.gui.sideBySide=this.createSideBySide(),this._rendered=this.element;var u=this.codemirror;setTimeout(function(){u.refresh()}.bind(u),0)}},B.prototype.autosaveLine truncated
|
||||
header:o[1].replace(/^ *| *\| *$/g,"").split(/ *\| */),align:o[2].replace(/^ *|\| *$/g,"").split(/ *\| */),cells:o[3].replace(/(?: *\| *)?\n$/,"").split("\n")},u=0;u<s.align.length;u++)/^ *-+: *$/.test(s.align[u])?s.align[u]="right":/^ *:-+: *$/.test(s.align[u])?s.align[u]="center":/^ *:-+ *$/.test(s.align[u])?s.align[u]="left":s.align[u]=null;for(u=0;u<s.cells.length;u++)s.cells[u]=s.cells[u].replace(/^ *\| *| *\| *$/g,"").split(/ *\| */);this.tokens.push(s)}else if(t&&(o=this.rules.paragraph.exec(e)))e=e.substring(o[0].length),this.tokens.push({type:"paragraph",text:"\n"===o[1].charAt(o[1].length-1)?o[1].slice(0,-1):o[1]});else if(o=this.rules.text.exec(e))e=e.substring(o[0].length),this.tokens.push({type:"text",text:o[0]});else if(e)throw new Error("Infinite loop on byte: "+e.charCodeAt(0));return this.tokens};var p={escape:/^\\([\\`*{}\[\]()#+\-.!_>])/,autolink:/^<([^ >]+(@|:\/)[^ >]+)>/,url:u,tag:/^<!--[\s\S]*?-->|^<\/?\w+(?:"[^"]*"|'[^']*'|[^'">])*?>/,link:/^!?\[(inside)\]\(href\)/,reflink:/^!?\[(inside)\]\s*\[([^\]]*)\]/,nolink:/^!?\[((?:\[[^\]]*\]|[^\[\]])*)\]/,strong:/^__([\s\S]+?)__(?!_)|^\*\*([\s\S]+?)\*\*(?!\*)/,em:/^\b_((?:[^_]|__)+?)_\b|^\*((?:\*\*|[\s\S])+?)\*(?!\*)/,code:/^(`+)\s*([\s\S]*?[^`])\s*\1(?!`)/,br:/^ {2,}\n(?!\s*$)/,del:u,text:/^[\s\S]+?(?=[\\<!\[_*`]| {2,}\n|$)/};p._inside=/(?:\[[^\]]*\]|[^\[\]]|\](?=[^\[]*\]))*/,p._href=/\s*<?([\s\S]*?)>?(?:\s+['"]([\s\S]*?)['"])?\s*/,p.link=c(p.link)("inside",p._inside)("href",p._href)(),p.reflink=c(p.reflink)("inside",p._inside)(),p.normal=f({},p),p.pedantic=f({},p.normal,{strong:/^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/,em:/^_(?=\S)([\s\S]*?\S)_(?!_)|^\*(?=\S)([\s\S]*?\S)\*(?!\*)/}),p.gfm=f({},p.normal,{escape:c(p.escape)("])","~|])")(),url:/^(https?:\/\/[^\s<]+[^<.,:;"')\]\s])/,del:/^~~(?=\S)([\s\S]*?\S)~~/,text:c(p.text)("]|","~]|")("|","|https?://|")()}),p.breaks=f({},p.gfm,{br:c(p.br)("{2,}","*")(),text:c(p.gfm.text)("{2,}","*")()}),i.rules=p,i.output=function(e,t,n){var r=new i(t,n);return r.output(e)},i.prototype.output=function(e){for(var t,n,r,i,o="";e;)if(i=this.rules.escape.exec(e))e=e.substring(i[0].length),o+=i[1];else if(i=this.rules.autolink.exec(e))e=e.substring(i[0].length),"@"===i[2]?(n=":"===i[1].charAt(6)?this.mangle(i[1].substring(7)):this.mangle(i[1]),r=this.mangle("mailto:")+n):(n=l(i[1]),r=n),o+=this.renderer.link(r,null,n);else if(this.inLink||!(i=this.rules.url.exec(e))){if(i=this.rules.tag.exec(e))!this.inLink&&/^<a /i.test(i[0])?this.inLink=!0:this.inLink&&/^<\/a>/i.test(i[0])&&(this.inLink=!1),e=e.substring(i[0].length),o+=this.options.sanitize?this.options.sanitizer?this.options.sanitizer(i[0]):l(i[0]):i[0];else if(i=this.rules.link.exec(e))e=e.substring(i[0].length),this.inLink=!0,o+=this.outputLink(i,{href:i[2],title:i[3]}),this.inLink=!1;else if((i=this.rules.reflink.exec(e))||(i=this.rules.nolink.exec(e))){if(e=e.substring(i[0].length),t=(i[2]||i[1]).replace(/\s+/g," "),t=this.links[t.toLowerCase()],!t||!t.href){o+=i[0].charAt(0),e=i[0].substring(1)+e;continue}this.inLink=!0,o+=this.outputLink(i,t),this.inLink=!1}else if(i=this.rules.strong.exec(e))e=e.substring(i[0].length),o+=this.renderer.strong(this.output(i[2]||i[1]));else if(i=this.rules.em.exec(e))e=e.substring(i[0].length),o+=this.renderer.em(this.output(i[2]||i[1]));else if(i=this.rules.code.exec(e))e=e.substring(i[0].length),o+=this.renderer.codespan(l(i[2],!0));else if(i=this.rules.br.exec(e))e=e.substring(i[0].length),o+=this.renderer.br();else if(i=this.rules.del.exec(e))e=e.substring(i[0].length),o+=this.renderer.del(this.output(i[1]));else if(i=this.rules.text.exec(e))e=e.substring(i[0].length),o+=this.renderer.text(l(this.smartypants(i[0])));else if(e)throw new Error("Infinite loop on byte: "+e.charCodeAt(0))}else e=e.substring(i[0].length),n=l(i[1]),r=n,o+=this.renderer.link(r,null,n);return o},i.prototype.outputLink=function(e,t){var n=l(t.href),r=t.title?l(t.title):null;return"!"!==e[0].charAt(0)?this.renderer.link(n,r,this.output(e[1])):this.renderer.image(n,r,l(e[1]))},i.prototype.smartypants=function(e){return this.options.smartypants?e.replace(/---/g,"—").replace(/--/g,"–").replace(/(^|[-\u2014\/(\[{"\s])'/g,"$1‘").replace(/'/g,"’").replace(/(^|[-\u2014\/(\[{\u2018\s])"/g,"$1“").replace(/"/g,"”").replace(/\.{3}/g,"…"):e},i.prototype.mangle=function(e){if(!this.options.mangle)return e;for(var t,n="",r=e.length,i=0;r>i;i++)t=e.charCodeAt(i),Math.random()>.5&&(t="x"+t.toString(16)),n+="&#"+t+";";return n},o.prototype.code=function(e,t,n){if(this.options.highlight){var r=this.options.highlight(e,t);null!=r&&r!==e&&(n=!0,e=r)}return t?'<pre><code class="'+this.options.langPrefix+l(t,!0)+'">'+(n?e:l(e,!0))+"\n</code></pre>\n":"<pre><code>"+(n?e:l(e,!0))+"\n</code></pre>"},o.prototype.blockquote=function(e){return"<blockquote>\n"+e+"</blockquote>\n"},o.prototype.html=function(e){return e},o.prototype.heading=function(e,t,n){return"<h"+t+' id="'+this.options.headerPrefix+n.toLowerCase().replace(/[^\w]+/g,"-"Line truncated
|
||||
}(navigator.userAgent||navigator.vendor||window.opera),e},V="",K={bold:{name:"bold",action:c,className:"fa fa-bold",title:"Bold","default":!0},italic:{name:"italic",action:u,className:"fa fa-italic",title:"Italic","default":!0},strikethrough:{name:"strikethrough",action:f,className:"fa fa-strikethrough",title:"Strikethrough"},heading:{name:"heading",action:p,className:"fa fa-header",title:"Heading","default":!0},"heading-smaller":{name:"heading-smaller",action:p,className:"fa fa-header fa-header-x fa-header-smaller",title:"Smaller Heading"},"heading-bigger":{name:"heading-bigger",action:m,className:"fa fa-header fa-header-x fa-header-bigger",title:"Bigger Heading"},"heading-1":{name:"heading-1",action:g,className:"fa fa-header fa-header-x fa-header-1",title:"Big Heading"},"heading-2":{name:"heading-2",action:v,className:"fa fa-header fa-header-x fa-header-2",title:"Medium Heading"},"heading-3":{name:"heading-3",action:y,className:"fa fa-header fa-header-x fa-header-3",title:"Small Heading"},"separator-1":{name:"separator-1"},code:{name:"code",action:h,className:"fa fa-code",title:"Code"},quote:{name:"quote",action:d,className:"fa fa-quote-left",title:"Quote","default":!0},"unordered-list":{name:"unordered-list",action:x,className:"fa fa-list-ul",title:"Generic List","default":!0},"ordered-list":{name:"ordered-list",action:b,className:"fa fa-list-ol",title:"Numbered List","default":!0},"clean-block":{name:"clean-block",action:w,className:"fa fa-eraser fa-clean-block",title:"Clean block"},"separator-2":{name:"separator-2"},link:{name:"link",action:k,className:"fa fa-link",title:"Create Link","default":!0},image:{name:"image",action:S,className:"fa fa-picture-o",title:"Insert Image","default":!0},table:{name:"table",action:C,className:"fa fa-table",title:"Insert Table"},"horizontal-rule":{name:"horizontal-rule",action:L,className:"fa fa-minus",title:"Insert Horizontal Line"},"separator-3":{name:"separator-3"},preview:{name:"preview",action:A,className:"fa fa-eye no-disable",title:"Toggle Preview","default":!0},"side-by-side":{name:"side-by-side",action:N,className:"fa fa-columns no-disable no-mobile",title:"Toggle Side by Side","default":!0},fullscreen:{name:"fullscreen",action:s,className:"fa fa-arrows-alt no-disable no-mobile",title:"Toggle Fullscreen","default":!0},"separator-4":{name:"separator-4"},guide:{name:"guide",action:"https://simplemde.com/markdown-guide",className:"fa fa-question-circle",title:"Markdown Guide","default":!0},"separator-5":{name:"separator-5"},undo:{name:"undo",action:T,className:"fa fa-undo no-disable",title:"Undo"},redo:{name:"redo",action:M,className:"fa fa-repeat no-disable",title:"Redo"}},X={link:["[","](#url#)"],image:[""],table:["","\n\n| Column 1 | Column 2 | Column 3 |\n| -------- | -------- | -------- |\n| Text | Text | Text |\n\n"],horizontalRule:["","\n\n-----\n\n"]},Z={link:"URL for the link:",image:"URL of the image:"},J={bold:"**",code:"```",italic:"*"};B.marked=j,B.prototype.markdown=function(e){if(j){var t={};return this.options&&this.options.renderingConfig&&this.options.renderingConfig.singleLineBreaks===!1?t.breaks=!1:t.breaks=!0,this.options&&this.options.renderingConfig&&this.options.renderingConfig.codeSyntaxHighlighting===!0&&window.hljs&&(t.highlight=function(e){return window.hljs.highlightAuto(e).value}),j.setOptions(t),j(e)}},B.prototype.render=function(e){if(e||(e=this.element||document.getElementsByTagName("textarea")[0]),!this._rendered||this._rendered!==e){this.element=e;var t=this.options,n=this,i={};for(var o in t.shortcuts)null!==t.shortcuts[o]&&null!==q[o]&&!function(e){i[r(t.shortcuts[e])]=function(){q[e](n)}}(o);i.Enter="newlineAndIndentContinueMarkdownList",i.Tab="tabAndIndentMarkdownList",i["Shift-Tab"]="shiftTabAndUnindentMarkdownList",i.Esc=function(e){e.getOption("fullScreen")&&s(n)},document.addEventListener("keydown",function(e){e=e||window.event,27==e.keyCode&&n.codemirror.getOption("fullScreen")&&s(n)},!1);var a,l;if(t.spellChecker!==!1?(a="spell-checker",l=t.parsingConfig,l.name="gfm",l.gitHubSpice=!1,z({codeMirrorInstance:F})):(a=t.parsingConfig,a.name="gfm",a.gitHubSpice=!1),this.codemirror=F.fromTextArea(e,{mode:a,backdrop:l,theme:"paper",tabSize:void 0!=t.tabSize?t.tabSize:2,indentUnit:void 0!=t.tabSize?t.tabSize:2,indentWithTabs:t.indentWithTabs!==!1,lineNumbers:!1,autofocus:t.autofocus===!0,extraKeys:i,lineWrapping:t.lineWrapping!==!1,allowDropFileTypes:["text/plain"],placeholder:t.placeholder||e.getAttribute("placeholder")||"",styleSelectedText:void 0!=t.styleSelectedText?t.styleSelectedText:!0}),t.forceSync===!0){var c=this.codemirror;c.on("change",function(){c.save()})}this.gui={},t.toolbar!==!1&&(this.gui.toolbar=this.createToolbar()),t.status!==!1&&(this.gui.statusbar=this.createStatusbar()),void 0!=t.autosave&&t.autosave.enabled===!0&&this.autosave(),this.gui.sideBySide=this.createSideBySide(),this._rendered=this.element;var u=this.codemirror;setTimeout(function(){u.refresh()}.bind(u),0)}},B.prototyLine truncated
|
||||
@@ -7,8 +7,8 @@
|
||||
<link rel="stylesheet" href="{{ $root }}static/spectre.min.css">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<style type="text/css">
|
||||
.spacing-top { margin: 3px 0px 0px 0px; }
|
||||
.spacing-left { margin: 0px 3px 0px 0px; }
|
||||
.spacing-top { margin: 3px 0px 0px 0px !important; }
|
||||
.spacing-left { margin: 0px 3px 0px 0px !important; }
|
||||
.align-center { text-align: center; }
|
||||
.pipe { color: #e85600; border-color: #e85600; }
|
||||
.pipe:hover { background: #e85600; border-color: #e85600; }
|
||||
@@ -16,6 +16,20 @@
|
||||
.background-white { background: white; }
|
||||
.spacer-12px { width: 12px; display: inline-block; }
|
||||
</style>
|
||||
<script>window.NOTEBOOK_KB={{ keybindings }};</script>
|
||||
<script src="{{ $root }}static/shortcuts.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
NotebookShortcuts.annotate(document.getElementById('nav-home'), 'cancel');
|
||||
NotebookShortcuts.annotate(document.getElementById('nav-settings'), 'settings');
|
||||
NotebookShortcuts.annotate(document.getElementById('nav-new-page'), 'new_page');
|
||||
NotebookShortcuts.bind([
|
||||
{ action: 'new_page', handler: function() { document.getElementById('nav-new-page').click(); } },
|
||||
{ action: 'settings', handler: function() { document.getElementById('nav-settings').click(); } },
|
||||
{ action: 'cancel', handler: function() { document.getElementById('nav-home').click(); } }
|
||||
]);
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<section class="container grid-960 mt-10">
|
||||
@@ -25,7 +39,11 @@
|
||||
<a href="{{ $root }}" class="navbar-brand mr-10">Rizin Notebook</a>
|
||||
</section>
|
||||
<section class="navbar-section">
|
||||
<a href="{{ $root }}" class="btn btn-primary">Go Back</a>
|
||||
<a href="{{ $root }}settings" class="btn" id="nav-settings">Settings</a>
|
||||
|
||||
<a href="{{ $root }}new" class="btn" id="nav-new-page">New Page</a>
|
||||
|
||||
<a href="{{ $root }}" class="btn btn-primary" id="nav-home">Go Back</a>
|
||||
</section>
|
||||
</header>
|
||||
<section class="container">
|
||||
@@ -71,7 +89,7 @@
|
||||
<span class="input-group-addon detail-size">Build</span>
|
||||
</div>
|
||||
<div class="input-group spacing-top">
|
||||
<span class="input-group-addon detail-size">Location (RIZIN_PATH)</span>
|
||||
<span class="input-group-addon detail-size">Resolved Binary</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
|
||||
@@ -17,8 +17,14 @@
|
||||
<input type="text" disabled="" class="form-input" value="Error: {{ .error }}"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<a class="btn" href="{{ .root }}output/deleted">Delete</a>
|
||||
<button class="btn" type="button" onclick="window.parent.postMessage({type:'iframe-remove'},'*')">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(function(){
|
||||
var h = Math.max(document.body.scrollHeight, document.documentElement.scrollHeight);
|
||||
if (window.parent !== window) window.parent.postMessage({type:'iframe-resize',height:h},'*');
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -10,17 +10,83 @@
|
||||
<style type="text/css">
|
||||
html, body { width: 100%; height: 100%; margin: 0 }
|
||||
.button-edit { width: 75px ! important; margin-top: 9px; margin-right: 9px; }
|
||||
.nb-ac-wrap { position: relative; }
|
||||
.nb-ac-dropdown {
|
||||
position: absolute; top: 100%; left: 0; right: 0; z-index: 99999;
|
||||
background: white; border: 1px solid transparent; border-top: none;
|
||||
border-radius: 0 0 4px 4px; max-height: 260px; overflow-y: auto;
|
||||
display: none;
|
||||
}
|
||||
.nb-ac-item {
|
||||
padding: 5px 10px; cursor: pointer;
|
||||
font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
.nb-ac-item:hover, .nb-ac-item.nb-ac-sel { font-weight: bold; }
|
||||
.nb-ac-cmd { color: #1447e6; font-weight: bold; font-family: Inconsolata, monospace; margin-right: 4px; }
|
||||
.nb-ac-args { color: #178236; font-style: italic; font-family: Inconsolata, monospace; margin-right: 6px; }
|
||||
.nb-ac-summ { font-size: 12px; font-style: normal; opacity: .7; }
|
||||
</style>
|
||||
<script>window.NOTEBOOK_KB={{ keybindings }};</script>
|
||||
<script>window.NB_ROOT="{{ .root }}";</script>
|
||||
<script>window.NB_AC_CONFIG={{ autocomplete_config }};</script>
|
||||
<script src="{{ .root }}static/shortcuts.js"></script>
|
||||
<script src="{{ .root }}static/autocomplete.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<form style="margin-top: 15px; height: 100%;" action="{{ .root }}output/exec/{{ .unique }}" method="POST">
|
||||
{{ if .message }}
|
||||
<div class="toast toast-success" style="margin: 12px 12px 0 12px;">{{ .message }}</div>
|
||||
{{ end }}
|
||||
{{ if .error }}
|
||||
<div class="toast toast-error" style="margin: 12px 12px 0 12px;">{{ .error }}</div>
|
||||
{{ end }}
|
||||
<form id="cmdform" style="margin-top: 15px; height: 100%;" action="{{ .root }}output/exec/{{ .unique }}" method="POST">
|
||||
<div class="form-group">
|
||||
<input type="text" class="form-input" name="command" value="" placeholder="Rizin command for example: pdf"/>
|
||||
<input type="text" class="form-input command-input" id="cmdinput" name="command" value="{{ .command }}" placeholder="Rizin command (e.g. pdf, aaa, ii)" autofocus/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button class="btn btn-primary" type="submit">Exec</button>
|
||||
<a class="btn" href="{{ .root }}output/deleted">Cancel</a>
|
||||
<button class="btn" type="button" id="cancel-console" onclick="window.parent.postMessage({type:'iframe-remove'},'*')">Cancel</button>
|
||||
<span style="font-size:12px;margin-left:8px;opacity:.7;">Ctrl+Enter to execute</span>
|
||||
</div>
|
||||
</form>
|
||||
<script>
|
||||
var createdCellUnique = {{ if .cellUnique }}"{{ .cellUnique }}"{{ else }}""{{ end }};
|
||||
var createdCellLineCount = {{ if .lineCount }}{{ .lineCount }}{{ else }}0{{ end }};
|
||||
var createdCellWordCount = {{ if .wordCount }}{{ .wordCount }}{{ else }}0{{ end }};
|
||||
var createdCellActionTime = {{ if .actionTime }}"{{ .actionTime }}"{{ else }}""{{ end }};
|
||||
var input = document.getElementById('cmdinput');
|
||||
NotebookShortcuts.annotate(document.getElementById('cancel-console'), 'cancel');
|
||||
NotebookShortcuts.bind([
|
||||
{ action: 'cancel', allowInInputs: true, handler: function() { document.getElementById('cancel-console').click(); } },
|
||||
{ action: 'execute', allowInInputs: true, handler: function() { document.getElementById('cmdform').submit(); } }
|
||||
]);
|
||||
input.addEventListener('keydown', function(e) {
|
||||
if (e.ctrlKey && e.key === 'Enter') { e.preventDefault(); document.getElementById('cmdform').submit(); }
|
||||
});
|
||||
document.getElementById('cmdform').addEventListener('submit', function(e) {
|
||||
if (!input.value.trim()) { e.preventDefault(); window.parent.postMessage({type:'iframe-remove'},'*'); }
|
||||
});
|
||||
(function(){
|
||||
if (createdCellUnique && window.parent !== window) {
|
||||
window.parent.postMessage({
|
||||
type:'iframe-add-cell',
|
||||
cellType:'command',
|
||||
cellUnique:createdCellUnique,
|
||||
content:input.value,
|
||||
lineCount:createdCellLineCount,
|
||||
wordCount:createdCellWordCount,
|
||||
actionTime:createdCellActionTime,
|
||||
outputUrl:'{{ .root }}output/view/{{ .unique }}/' + createdCellUnique,
|
||||
deleteUrl:'{{ .root }}output/delete/{{ .unique }}/' + createdCellUnique
|
||||
}, '*');
|
||||
}
|
||||
var h = Math.max(document.body.scrollHeight, document.documentElement.scrollHeight);
|
||||
if (window.parent !== window) window.parent.postMessage({type:'iframe-resize',height:h},'*');
|
||||
input.focus();
|
||||
input.select();
|
||||
})();
|
||||
var acCfg = window.NB_AC_CONFIG || {};
|
||||
NotebookAutocomplete.attach(input, { apiUrl: '{{ .root }}api/v1/json/commands', maxResults: acCfg.max_results, minChars: acCfg.min_chars });
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,4 +1,4 @@
|
||||
{{ $error := .error }}{{ $llen := len .location }}<!DOCTYPE html>
|
||||
{{ $error := .error }}<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
@@ -7,10 +7,26 @@
|
||||
<link rel="stylesheet" href="{{ .root }}static/spectre.min.css">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<style type="text/css">
|
||||
.spacing-top { margin: 3px 0px 0px 0px; }
|
||||
.spacing-left { margin: 0px 3px 0px 0px; }
|
||||
.spacing-top { margin: 3px 0px 0px 0px !important; }
|
||||
.spacing-left { margin: 0px 3px 0px 0px !important; }
|
||||
.align-center { text-align: center; }
|
||||
</style>
|
||||
<script>window.NOTEBOOK_KB={{ keybindings }};</script>
|
||||
<script src="{{ .root }}static/shortcuts.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
NotebookShortcuts.annotate(document.getElementById('nav-home'), 'cancel');
|
||||
NotebookShortcuts.annotate(document.getElementById('nav-about'), 'about');
|
||||
NotebookShortcuts.annotate(document.getElementById('nav-new-page'), 'new_page');
|
||||
NotebookShortcuts.annotate(document.getElementById('nav-settings'), 'settings');
|
||||
NotebookShortcuts.bind([
|
||||
{ action: 'about', handler: function() { document.getElementById('nav-about').click(); } },
|
||||
{ action: 'new_page', handler: function() { document.getElementById('nav-new-page').click(); } },
|
||||
{ action: 'settings', handler: function() { document.getElementById('nav-settings').click(); } },
|
||||
{ action: 'cancel', handler: function() { document.getElementById('nav-home').click(); } }
|
||||
]);
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<section class="container grid-960 mt-10">
|
||||
@@ -20,7 +36,13 @@
|
||||
<a href="{{ .root }}" class="navbar-brand mr-10">Rizin Notebook</a>
|
||||
</section>
|
||||
<section class="navbar-section">
|
||||
<a href="{{ if lt $llen 1 }}{{ .root }}{{ else }}{{ .location }}{{ end }}" class="btn btn-primary">Go Back</a>
|
||||
<a href="{{ .root }}about" class="btn" id="nav-about">About</a>
|
||||
|
||||
<a href="{{ .root }}settings" class="btn" id="nav-settings">Settings</a>
|
||||
|
||||
<a href="{{ .root }}new" class="btn" id="nav-new-page">New Page</a>
|
||||
|
||||
<a href="{{ if .location }}{{ .location }}{{ else }}{{ .root }}{{ end }}" class="btn btn-primary" id="nav-home">Go Back</a>
|
||||
</section>
|
||||
</header>
|
||||
<section class="container">
|
||||
|
||||
@@ -13,7 +13,66 @@
|
||||
.pipe { color: #e85600; border-color: #e85600; }
|
||||
.pipe:hover { background: #e85600; border-color: #e85600; }
|
||||
.footer { position: absolute; bottom: 0px; padding: 5px; }
|
||||
.page-row { width: 100%; }
|
||||
.page-title {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
min-width: 0;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
.page-action {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.container, .columns, .column { overflow-x: hidden; }
|
||||
</style>
|
||||
<script>window.NOTEBOOK_KB={{ keybindings }};</script>
|
||||
<script src="{{ $root }}static/shortcuts.js"></script>
|
||||
<script>
|
||||
function confirmDelete(e, title) {
|
||||
e.preventDefault();
|
||||
if (!confirm('Delete "' + title + '" and all its data?')) return;
|
||||
window.location.href = e.target.closest('a').href;
|
||||
}
|
||||
function importRznb(input) {
|
||||
if (!input.files || !input.files[0]) return;
|
||||
var formData = new FormData();
|
||||
formData.append('file', input.files[0]);
|
||||
fetch('{{ $root }}api/v1/json/pages/import', {method: 'POST', body: formData})
|
||||
.then(function(r) {
|
||||
if (!r.ok) {
|
||||
return r.text().then(function(t) {
|
||||
try { var j = JSON.parse(t); throw new Error(j.error || 'Server error ' + r.status); }
|
||||
catch(e) { if (e.message) throw e; throw new Error('Server error ' + r.status); }
|
||||
});
|
||||
}
|
||||
return r.json();
|
||||
})
|
||||
.then(function(data) {
|
||||
if (data && data.page && data.page.id) {
|
||||
window.location.href = '{{ $root }}view/' + data.page.id;
|
||||
} else if (data && data.error) {
|
||||
alert(data.error);
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Import failed: unexpected response');
|
||||
location.reload();
|
||||
}
|
||||
})
|
||||
.catch(function(err) { alert('Import error: ' + (err.message || err)); });
|
||||
input.value = '';
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
NotebookShortcuts.annotate(document.getElementById('nav-settings'), 'settings');
|
||||
NotebookShortcuts.annotate(document.getElementById('nav-about'), 'about');
|
||||
NotebookShortcuts.annotate(document.getElementById('nav-new-page'), 'new_page');
|
||||
NotebookShortcuts.bind([
|
||||
{ action: 'about', handler: function() { document.getElementById('nav-about').click(); } },
|
||||
{ action: 'new_page', handler: function() { document.getElementById('nav-new-page').click(); } },
|
||||
{ action: 'settings', handler: function() { document.getElementById('nav-settings').click(); } }
|
||||
]);
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<section class="container grid-960 mt-10">
|
||||
@@ -23,11 +82,15 @@
|
||||
<a href="{{ $root }}" class="navbar-brand mr-10">Rizin Notebook</a>
|
||||
</section>
|
||||
<section class="navbar-section">
|
||||
<a href="{{ $root }}settings" class="btn">Settings</a>
|
||||
<a href="{{ $root }}settings" class="btn" id="nav-settings">Settings</a>
|
||||
|
||||
<a href="{{ $root }}about" class="btn">About</a>
|
||||
<a href="{{ $root }}about" class="btn" id="nav-about">About</a>
|
||||
|
||||
<a href="{{ $root }}new" class="btn btn-primary">New Page</a>
|
||||
<a href="{{ $root }}new" class="btn btn-primary" id="nav-new-page">New Page</a>
|
||||
|
||||
<label class="btn" style="cursor: pointer;">Import .rznb
|
||||
<input type="file" accept=".rznb" style="display:none" onchange="importRznb(this)">
|
||||
</label>
|
||||
</section>
|
||||
</header>
|
||||
<section class="container">
|
||||
@@ -38,18 +101,21 @@
|
||||
<span class="input-group-addon align-center">To start, create a new page.</span>
|
||||
</div>
|
||||
{{ else }}{{ range $i, $page := .list }}
|
||||
<div class="input-group spacing-top">
|
||||
<span class="input-group-addon spacing-left">{{ $page.title }}</span>
|
||||
<div class="input-group spacing-top page-row">
|
||||
<span class="input-group-addon spacing-left page-title" title="{{ $page.title }}">{{ $page.title }}</span>
|
||||
{{ if $page.pipe }}
|
||||
<button class="btn tooltip tooltip-bottom pipe spacing-left" data-tooltip="The pipe is still open."><i class="icon icon-more-horiz"></i></button>
|
||||
<button class="btn tooltip tooltip-bottom pipe spacing-left page-action" data-tooltip="The pipe is still open."><i class="icon icon-more-horiz"></i></button>
|
||||
{{ else }}
|
||||
<a class="btn spacing-left" href="{{ $root }}edit/{{ $page.unique }}">
|
||||
<a class="btn spacing-left page-action" href="{{ $root }}edit/{{ $page.unique }}">
|
||||
<i class="icon icon-edit">Edit</i>
|
||||
</a>
|
||||
{{ end }}
|
||||
<a class="btn spacing-left" href="{{ $root }}view/{{ $page.unique }}">
|
||||
<a class="btn spacing-left page-action" href="{{ $root }}view/{{ $page.unique }}">
|
||||
<i class="icon icon-forward">View</i>
|
||||
</a>
|
||||
<a class="btn spacing-left page-action" href="{{ $root }}delete/{{ $page.unique }}" onclick="confirmDelete(event, {{ stringify $page.title }})">
|
||||
<i class="icon icon-delete">Delete</i>
|
||||
</a>
|
||||
</div>
|
||||
{{ end }}{{ end }}
|
||||
</div>
|
||||
|
||||
@@ -11,15 +11,18 @@
|
||||
html, body { width: 100%; height: 100%; margin: 0 }
|
||||
.position-right { position: absolute; right: 0px; z-index: 99999; }
|
||||
.button-edit { width: 75px ! important; margin-top: 9px; margin-right: 9px; }
|
||||
.CodeMirror .nb-search-hit { background: #ffe08a; color: #111111; border-radius: 2px; }
|
||||
</style>
|
||||
<script>window.NOTEBOOK_KB={{ keybindings }};</script>
|
||||
<script src="{{ .root }}static/shortcuts.js"></script>
|
||||
</head>
|
||||
<body onload="editor()">
|
||||
<div style="margin-top: 15px; height: 100%;">
|
||||
<form action="{{ .root }}markdown/save{{ .path }}" method="POST">
|
||||
<div class="position-right">
|
||||
<button class="btn btn-action button-edit" type="submit"><i class="icon icon-share">Code</i> Save</button>
|
||||
<a class="btn btn-action button-edit" href="{{ .root }}markdown/delete{{ .path }}"><i class="icon icon-cross">Code</i> Delete</a>
|
||||
<a class="btn btn-action button-edit" href="{{ .root }}markdown/view{{ .path }}"><i class="icon icon-edit">Code</i> Cancel</a>
|
||||
<button class="btn btn-action button-edit" type="submit" id="save-markdown"><i class="icon icon-share">Code</i> Save</button>
|
||||
<button class="btn btn-action button-edit" type="button" onclick="if(confirm('Delete this cell?')){fetch('{{ .root }}markdown/delete{{ .path }}',{redirect:'follow'}).then(function(){window.parent.postMessage({type:'iframe-remove'},'*')})}"><i class="icon icon-cross">Code</i> Delete</button>
|
||||
<a class="btn btn-action button-edit" href="{{ .root }}markdown/view{{ .path }}" id="cancel-markdown"><i class="icon icon-edit">Code</i> Cancel</a>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<textarea class="form-input" id="input-body" name="markdown" placeholder="Content" cols="80" rows="24">{{printf "%s" .raw}}</textarea>
|
||||
@@ -28,8 +31,42 @@
|
||||
</div>
|
||||
<script src="{{ .root }}static/simplemde.min.js"></script>
|
||||
<script>
|
||||
var markdownSearchMarks = [];
|
||||
function clearMarkdownSearchMarks() {
|
||||
while (markdownSearchMarks.length > 0) {
|
||||
markdownSearchMarks.pop().clear();
|
||||
}
|
||||
}
|
||||
function parseMarkdownSearchRanges(raw) {
|
||||
if (!raw) return [];
|
||||
return raw.split(',').map(function(part) {
|
||||
var pieces = part.split(':');
|
||||
return {
|
||||
start: Number(pieces[0] || 0),
|
||||
end: Number(pieces[1] || 0)
|
||||
};
|
||||
}).filter(function(range) {
|
||||
return range.end > range.start;
|
||||
});
|
||||
}
|
||||
function applyMarkdownSearchHighlight(editor, lineNumber, ranges) {
|
||||
clearMarkdownSearchMarks();
|
||||
if (!editor || lineNumber <= 0 || !ranges || !ranges.length) return;
|
||||
var targetLine = lineNumber - 1;
|
||||
for (var i = 0; i < ranges.length; i++) {
|
||||
markdownSearchMarks.push(editor.markText(
|
||||
{ line: targetLine, ch: ranges[i].start },
|
||||
{ line: targetLine, ch: ranges[i].end },
|
||||
{ className: 'nb-search-hit' }
|
||||
));
|
||||
}
|
||||
editor.setCursor({ line: targetLine, ch: ranges[0].start });
|
||||
editor.scrollIntoView({ line: targetLine, ch: ranges[0].start }, 140);
|
||||
editor.focus();
|
||||
}
|
||||
function editor() {
|
||||
var simplemde = new SimpleMDE({
|
||||
var simplemde;
|
||||
simplemde = new SimpleMDE({
|
||||
element: document.getElementById("input-body"),
|
||||
autofocus: true,
|
||||
hideIcons: ["side-by-side", "fullscreen", "guide"],
|
||||
@@ -37,9 +74,37 @@
|
||||
spellChecker: false,
|
||||
forceSync: true,
|
||||
indentWithTabs: false,
|
||||
previewRender: function(plainText) {
|
||||
return NotebookSafeMarkdown.buildFragment(plainText, simplemde);
|
||||
},
|
||||
promptURLs: true,
|
||||
tabSize: 4,
|
||||
});
|
||||
var cm = simplemde.codemirror;
|
||||
var params = new URLSearchParams(window.location.search);
|
||||
applyMarkdownSearchHighlight(
|
||||
cm,
|
||||
Number(params.get('search_line') || 0),
|
||||
parseMarkdownSearchRanges(params.get('search_ranges') || '')
|
||||
);
|
||||
window.addEventListener('message', function(event) {
|
||||
if (!event.data || !event.data.type) return;
|
||||
if (event.data.type === 'notebook-search-clear') {
|
||||
clearMarkdownSearchMarks();
|
||||
} else if (event.data.type === 'notebook-search-highlight-editor') {
|
||||
applyMarkdownSearchHighlight(cm, Number(event.data.lineNumber || 0), event.data.ranges || []);
|
||||
}
|
||||
});
|
||||
setTimeout(function() {
|
||||
var h = Math.max(document.body.scrollHeight, document.documentElement.scrollHeight);
|
||||
if (window.parent !== window) window.parent.postMessage({type:'iframe-resize',height:h},'*');
|
||||
}, 100);
|
||||
NotebookShortcuts.annotate(document.getElementById('save-markdown'), 'save');
|
||||
NotebookShortcuts.annotate(document.getElementById('cancel-markdown'), 'cancel');
|
||||
NotebookShortcuts.bind([
|
||||
{ action: 'save', allowInInputs: true, handler: function() { document.getElementById('save-markdown').click(); } },
|
||||
{ action: 'cancel', allowInInputs: true, handler: function() { document.getElementById('cancel-markdown').click(); } }
|
||||
]);
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@@ -11,13 +11,15 @@
|
||||
.button-edit { float: right; width: 60px ! important; margin-top: 5px; }
|
||||
.markdown-container { margin-top: 9px; }
|
||||
</style>
|
||||
<script>window.NOTEBOOK_KB={{ keybindings }};</script>
|
||||
<script src="{{ .root }}static/shortcuts.js"></script>
|
||||
</head>
|
||||
<body onload="view()">
|
||||
<a class="btn btn-action spacing-left button-edit" href="{{ .root }}markdown/edit{{ .path }}">
|
||||
<a class="btn btn-action spacing-left button-edit" href="{{ .root }}markdown/edit{{ .path }}" id="edit-markdown">
|
||||
<i class="icon icon-edit">Code</i> Edit
|
||||
</a>
|
||||
<div class="markdown-container" id="markdown-container" >
|
||||
<textarea style="display: none;" name="md-container" id="md-container" cols="30" rows="10">{{ .html }}</textarea>
|
||||
<textarea style="display: none;" name="md-container" id="md-container" cols="30" rows="10">{{ .raw }}</textarea>
|
||||
</div>
|
||||
<script src="{{ .root }}static/simplemde.min.js"></script>
|
||||
<script>
|
||||
@@ -26,13 +28,26 @@
|
||||
element: document.getElementById('md-container'),
|
||||
autoDownloadFontAwesome: false,
|
||||
spellChecker: false,
|
||||
toolbar: false,
|
||||
status: false,
|
||||
indentWithTabs: false,
|
||||
promptURLs: true,
|
||||
tabSize: 4,
|
||||
});
|
||||
var md = smde.value();
|
||||
document.getElementById('markdown-container').innerHTML = smde.markdown(md);
|
||||
NotebookSafeMarkdown.renderInto(
|
||||
document.getElementById('markdown-container'),
|
||||
md,
|
||||
smde
|
||||
);
|
||||
setTimeout(function() {
|
||||
var h = Math.max(document.body.scrollHeight, document.documentElement.scrollHeight);
|
||||
if (window.parent !== window) window.parent.postMessage({type:'iframe-resize',height:h},'*');
|
||||
}, 50);
|
||||
NotebookShortcuts.annotate(document.getElementById('edit-markdown'), 'edit_markdown');
|
||||
NotebookShortcuts.bind([
|
||||
{ action: 'edit_markdown', handler: function() { document.getElementById('edit-markdown').click(); } }
|
||||
]);
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@@ -21,10 +21,248 @@
|
||||
}
|
||||
@font-face { font-family: 'Inconsolata'; src: URL('{{ .root }}static/inconsolata.ttf') format('truetype'); }
|
||||
html, body { width: 100%; height: 100%; background-color: #4b4b4b; color: #eeeeee; font-family: Inconsolata; }
|
||||
pre { width: 100%; height: 100%; margin: 5px; background-color: inherit; color: inherit; line-height: 8px; }
|
||||
pre { width: 100%; height: 100%; margin: 5px; background-color: inherit; color: inherit; line-height: 10px; }
|
||||
table {
|
||||
border-collapse: separate;
|
||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.15);
|
||||
background-color: inherit;
|
||||
color: inherit;
|
||||
height: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
th {
|
||||
font-family: sans-serif;
|
||||
text-align: left;
|
||||
}
|
||||
td, th {
|
||||
padding: 3px 10px;
|
||||
}
|
||||
.nb-search-hit {
|
||||
background: #ffe08a;
|
||||
color: #111111;
|
||||
border-radius: 2px;
|
||||
padding: 0 1px;
|
||||
}
|
||||
tbody tr {
|
||||
border-bottom: 1px solid #dddddd;
|
||||
}
|
||||
tbody tr:nth-of-type(even) {
|
||||
-webkit-backdrop-filter: invert(40%);
|
||||
backdrop-filter: invert(40%);
|
||||
}
|
||||
tbody tr:last-of-type {
|
||||
border-bottom: 2px solid #009879;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<pre>{{ .output | raw }}</pre>
|
||||
<div id="output-root">{{ .output }}</div>
|
||||
<script>
|
||||
(function(){
|
||||
var rawOutput = {{ .raw_output | stringify }};
|
||||
|
||||
function normalizeText(text) {
|
||||
return String(text || '').replace(/\r\n?/g, '\n');
|
||||
}
|
||||
function notifySize(){
|
||||
var h = Math.max(document.body.scrollHeight, document.documentElement.scrollHeight);
|
||||
if (window.parent !== window) window.parent.postMessage({type:'iframe-resize',height:h},'*');
|
||||
}
|
||||
function unwrapMarks(root) {
|
||||
var marks = root.querySelectorAll('mark.nb-search-hit');
|
||||
for (var i = 0; i < marks.length; i++) {
|
||||
var mark = marks[i];
|
||||
var parent = mark.parentNode;
|
||||
while (mark.firstChild) {
|
||||
parent.insertBefore(mark.firstChild, mark);
|
||||
}
|
||||
parent.removeChild(mark);
|
||||
}
|
||||
root.normalize();
|
||||
}
|
||||
function codePointToCodeUnitOffset(text, codePointOffset) {
|
||||
if (codePointOffset <= 0) return 0;
|
||||
return Array.from(text).slice(0, codePointOffset).join('').length;
|
||||
}
|
||||
function childOffset(node) {
|
||||
var offset = 0;
|
||||
while (node && node.previousSibling) {
|
||||
node = node.previousSibling;
|
||||
offset++;
|
||||
}
|
||||
return offset;
|
||||
}
|
||||
function buildTextSegments(root) {
|
||||
var segments = [];
|
||||
var codePointIndex = 0;
|
||||
|
||||
function walk(node) {
|
||||
if (!node) return;
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
var length = Array.from(node.data || '').length;
|
||||
if (!length) return;
|
||||
segments.push({ type: 'text', node: node, start: codePointIndex, end: codePointIndex + length });
|
||||
codePointIndex += length;
|
||||
return;
|
||||
}
|
||||
if (node.nodeType !== Node.ELEMENT_NODE) return;
|
||||
if (node.tagName === 'BR') {
|
||||
segments.push({
|
||||
type: 'break',
|
||||
parent: node.parentNode,
|
||||
offset: childOffset(node),
|
||||
start: codePointIndex,
|
||||
end: codePointIndex + 1
|
||||
});
|
||||
codePointIndex++;
|
||||
return;
|
||||
}
|
||||
for (var child = node.firstChild; child; child = child.nextSibling) {
|
||||
walk(child);
|
||||
}
|
||||
}
|
||||
|
||||
walk(root);
|
||||
return segments;
|
||||
}
|
||||
function boundaryBeforeSegment(segment) {
|
||||
if (!segment) return null;
|
||||
if (segment.type === 'break') {
|
||||
return { container: segment.parent, offset: segment.offset };
|
||||
}
|
||||
return { container: segment.node, offset: 0 };
|
||||
}
|
||||
function boundaryAfterSegment(segment) {
|
||||
if (!segment) return null;
|
||||
if (segment.type === 'break') {
|
||||
return { container: segment.parent, offset: segment.offset + 1 };
|
||||
}
|
||||
return {
|
||||
container: segment.node,
|
||||
offset: codePointToCodeUnitOffset(segment.node.data || '', segment.end - segment.start)
|
||||
};
|
||||
}
|
||||
function locateBoundary(root, segments, absoluteOffset) {
|
||||
if (!segments.length) {
|
||||
return { container: root, offset: 0 };
|
||||
}
|
||||
for (var i = 0; i < segments.length; i++) {
|
||||
var segment = segments[i];
|
||||
if (absoluteOffset < segment.start) {
|
||||
return boundaryBeforeSegment(segment);
|
||||
}
|
||||
if (segment.type === 'break') {
|
||||
if (absoluteOffset === segment.start) {
|
||||
return boundaryBeforeSegment(segment);
|
||||
}
|
||||
if (absoluteOffset === segment.end) {
|
||||
return boundaryAfterSegment(segment);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (absoluteOffset <= segment.end) {
|
||||
return {
|
||||
container: segment.node,
|
||||
offset: codePointToCodeUnitOffset(segment.node.data || '', Math.max(0, absoluteOffset - segment.start))
|
||||
};
|
||||
}
|
||||
}
|
||||
return boundaryAfterSegment(segments[segments.length - 1]);
|
||||
}
|
||||
function wrapCodePointRange(root, start, end) {
|
||||
var segments = buildTextSegments(root);
|
||||
var startBoundary = locateBoundary(root, segments, start);
|
||||
var endBoundary = locateBoundary(root, segments, end);
|
||||
if (!startBoundary || !endBoundary) return null;
|
||||
|
||||
var range = document.createRange();
|
||||
range.setStart(startBoundary.container, startBoundary.offset);
|
||||
range.setEnd(endBoundary.container, endBoundary.offset);
|
||||
if (range.collapsed) return null;
|
||||
|
||||
var mark = document.createElement('mark');
|
||||
mark.className = 'nb-search-hit';
|
||||
mark.appendChild(range.extractContents());
|
||||
range.insertNode(mark);
|
||||
return mark;
|
||||
}
|
||||
function normalizeRanges(ranges, length) {
|
||||
if (!ranges || !ranges.length) return [];
|
||||
return ranges.map(function(range) {
|
||||
return {
|
||||
start: Math.max(0, Math.min(length, Number(range.start) || 0)),
|
||||
end: Math.max(0, Math.min(length, Number(range.end) || 0))
|
||||
};
|
||||
}).filter(function(range) {
|
||||
return range.end > range.start;
|
||||
});
|
||||
}
|
||||
function lineBounds(text, lineNumber) {
|
||||
if (lineNumber <= 0) return null;
|
||||
var chars = Array.from(normalizeText(text));
|
||||
var start = 0;
|
||||
var currentLine = 1;
|
||||
while (start < chars.length && currentLine < lineNumber) {
|
||||
if (chars[start] === '\n') {
|
||||
currentLine++;
|
||||
}
|
||||
start++;
|
||||
}
|
||||
if (currentLine !== lineNumber) return null;
|
||||
var end = start;
|
||||
while (end < chars.length && chars[end] !== '\n') {
|
||||
end++;
|
||||
}
|
||||
return { start: start, end: end };
|
||||
}
|
||||
function highlightSearchResult(message) {
|
||||
var root = document.getElementById('output-root');
|
||||
if (!root) return;
|
||||
|
||||
unwrapMarks(root);
|
||||
|
||||
var bounds = lineBounds(rawOutput, Number(message.lineNumber || 0));
|
||||
if (!bounds) return;
|
||||
|
||||
var ranges = normalizeRanges(message.ranges || [], bounds.end - bounds.start).sort(function(a, b) {
|
||||
return (b.start || 0) - (a.start || 0);
|
||||
});
|
||||
var firstMark = null;
|
||||
for (var i = 0; i < ranges.length; i++) {
|
||||
var range = ranges[i];
|
||||
var start = bounds.start + Number(range.start || 0);
|
||||
var end = bounds.start + Number(range.end || 0);
|
||||
if (end <= start) continue;
|
||||
var mark = wrapCodePointRange(root, start, end);
|
||||
if (mark) {
|
||||
firstMark = mark;
|
||||
}
|
||||
}
|
||||
|
||||
if (firstMark && firstMark.scrollIntoView) {
|
||||
firstMark.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
||||
}
|
||||
notifySize();
|
||||
}
|
||||
|
||||
window.addEventListener('message', function(event) {
|
||||
if (!event.data || !event.data.type) return;
|
||||
if (event.data.type === 'notebook-search-clear') {
|
||||
var root = document.getElementById('output-root');
|
||||
if (root) {
|
||||
unwrapMarks(root);
|
||||
notifySize();
|
||||
}
|
||||
} else if (event.data.type === 'notebook-search-highlight') {
|
||||
highlightSearchResult(event.data);
|
||||
}
|
||||
});
|
||||
rawOutput = normalizeText(rawOutput);
|
||||
if (document.readyState === 'complete') notifySize();
|
||||
else window.addEventListener('load', notifySize);
|
||||
setTimeout(notifySize, 100);
|
||||
setTimeout(notifySize, 500);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
@@ -7,12 +7,14 @@
|
||||
<link rel="stylesheet" href="{{ $root }}static/spectre.min.css">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<style type="text/css">
|
||||
.spacing-top { margin: 3px 0px 0px 0px; }
|
||||
.spacing-left { margin: 0px 3px 0px 0px; }
|
||||
.spacing-top { margin: 3px 0px 0px 0px !important; }
|
||||
.spacing-left { margin: 0px 3px 0px 0px !important; }
|
||||
.navbar-fixed-height { height: 32px; }
|
||||
.icon-height { height: 20px; }
|
||||
iframe { border: 0px; width: 100%; height: 100% }
|
||||
</style>
|
||||
<script>window.NOTEBOOK_KB={{ keybindings }};</script>
|
||||
<script src="{{ $root }}static/shortcuts.js"></script>
|
||||
<script type="text/javascript">
|
||||
function confirm_delete(event) {
|
||||
if (!window.confirm("Are you sure you want to delete it?")) {
|
||||
@@ -21,15 +23,21 @@
|
||||
}
|
||||
function verify(event) {
|
||||
var title = document.getElementById('title').value.trim().length;
|
||||
var files = document.getElementById('binary').files.length;
|
||||
if (title < 1) {
|
||||
alert("Title field is empty. Please add one.");
|
||||
event.preventDefault();
|
||||
} else if (files < 1) {
|
||||
alert("You forgot to select a file to analyze. Please select one.");
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
NotebookShortcuts.annotate(document.getElementById('nav-about'), 'about');
|
||||
NotebookShortcuts.annotate(document.getElementById('save-page'), 'save');
|
||||
NotebookShortcuts.bind([
|
||||
{ action: 'about', handler: function() { document.getElementById('nav-about').click(); } },
|
||||
{ action: 'new_page', handler: function() { window.location.href = '{{ $root }}new'; } },
|
||||
{ action: 'save', allowInInputs: true, handler: function() { document.getElementById('save-page').click(); } },
|
||||
{ action: 'cancel', handler: function() { document.getElementById('cancel-page').click(); } }
|
||||
]);
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
@@ -40,6 +48,8 @@
|
||||
<a href="{{ $root }}" class="navbar-brand mr-10">Rizin Notebook</a>
|
||||
</section>
|
||||
<section class="navbar-section">
|
||||
<a href="{{ $root }}about" class="btn" id="nav-about">About</a>
|
||||
|
||||
{{ $page.title }}
|
||||
</section>
|
||||
</header>
|
||||
@@ -50,15 +60,18 @@
|
||||
<div class="form-group">
|
||||
<input type="hidden" name="unique" value="{{ .unique }}" />
|
||||
<input type="text" class="form-input" name="title" id="title" value="{{ .title }}" placeholder="Notebook page title"/>
|
||||
{{ if lt $nlen 1 }}
|
||||
<br>
|
||||
<label for="binary">Choose a binary to open: </label>
|
||||
<label for="binary">{{ if lt $nlen 1 }}Optional binary to open{{ else }}Attach or replace binary{{ end }}: </label>
|
||||
<input type="file" id="binary" name="binary">
|
||||
{{ if gt $nlen 0 }}
|
||||
<p class="form-input-hint">Current binary: {{ if $page.filename }}{{ $page.filename }}{{ else }}none attached{{ end }}</p>
|
||||
{{ else }}
|
||||
<p class="form-input-hint">You can create notes without a binary now and attach one later from the edit page.</p>
|
||||
{{ end }}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button class="btn btn-primary" type="submit">{{ if lt $nlen 1 }}Create{{ else }}Save{{ end }}</button>
|
||||
<a class="btn" href="{{ $root }}">Cancel</a>
|
||||
<button class="btn btn-primary" type="submit" id="save-page">{{ if lt $nlen 1 }}Create{{ else }}Save{{ end }}</button>
|
||||
<a class="btn" href="{{ $root }}" id="cancel-page">Cancel</a>
|
||||
{{ if gt $nlen 0 }}
|
||||
<a class="btn float-right" href="{{ $root }}delete/{{ .unique }}" onclick="confirm_delete(event)">Delete</a>
|
||||
{{ end }}
|
||||
|
||||
@@ -7,37 +7,425 @@
|
||||
<link rel="stylesheet" href="{{ $root }}static/spectre.min.css">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<style type="text/css">
|
||||
.spacing-top { margin: 3px 0px 0px 0px; }
|
||||
.spacing-left { margin: 0px 3px 0px 0px; }
|
||||
.align-center { text-align: center; }
|
||||
.navbar-fixed-height { height: 32px; }
|
||||
.icon-height { height: 20px; }
|
||||
.title-margin { margin-top: auto; margin-bottom: auto; margin-right: 10px; }
|
||||
.md-heigh { height: 64px; }
|
||||
.pipe { color: #e85600; border-color: #e85600; }
|
||||
.pipe:hover { background: #e85600; border-color: #e85600; }
|
||||
iframe { border: 0px; width: 100%; height: 100% }
|
||||
.resizer { display: flex; margin: 0; padding: 0; resize: vertical; overflow: hidden }
|
||||
.resizer > .resized { flex-grow: 1; margin: 0; padding: 0; border: 0 }
|
||||
.syntax-menu { right: 35px; }
|
||||
.align-right { text-align: right; }
|
||||
</style>
|
||||
<script>
|
||||
function handle(obj) {
|
||||
obj.style.height = (obj.contentWindow.document.documentElement.scrollHeight + 20) + 'px';
|
||||
if (obj.contentWindow.location.href.indexOf("/deleted") > 0) {
|
||||
obj.parentNode.parentNode.removeChild(obj.parentNode);
|
||||
@font-face { font-family: 'Inconsolata'; src: URL('{{ .root }}static/inconsolata.ttf') format('truetype'); }
|
||||
.spacing-top { margin: 3px 0px 0px 0px !important; }
|
||||
.spacing-left { margin: 0px 3px 0px 0px !important; }
|
||||
.align-center { text-align: center; }
|
||||
.navbar-fixed-height { height: 32px; }
|
||||
.icon-height { height: 20px; }
|
||||
.title-margin { margin-top: auto; margin-bottom: auto; margin-right: 10px; }
|
||||
.md-heigh { height: 64px; }
|
||||
.cell-group { position: relative; margin-bottom: 2px; }
|
||||
.cell-footer {
|
||||
font-size: 11px;
|
||||
text-align: right;
|
||||
padding: 3px 8px 0 8px;
|
||||
font-family: Inconsolata, monospace;
|
||||
opacity: .7;
|
||||
}
|
||||
.cell-footer span { margin-left: 12px; }
|
||||
.pipe { color: #e85600; border-color: #e85600; }
|
||||
.pipe:hover { background: #e85600; border-color: #e85600; }
|
||||
iframe { border: 0px; width: 100%; height: 100% }
|
||||
.resizer { display: flex; margin: 0; padding: 0; resize: vertical; overflow: hidden }
|
||||
.resizer > .resized { flex-grow: 1; margin: 0; padding: 0; border: 0 }
|
||||
.syntax-menu { right: 35px; }
|
||||
.align-right { text-align: right; }
|
||||
textarea {
|
||||
margin: 5px;
|
||||
font-family: Inconsolata;
|
||||
background-color: inherit;
|
||||
color: inherit;
|
||||
line-height: 8px;
|
||||
resize: none;
|
||||
}
|
||||
.button-help { width: 32px; float: left; }
|
||||
.command-input { margin: 0px 0px 0px 36px; display: flex; width: calc(100% - 36px); min-width: 0; }
|
||||
.modal-box { width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.8); }
|
||||
.modal-search { width: 80%; height: 80%; margin: 10px; }
|
||||
.modal-results { width: 100%; max-height: 500px; position: initial; overflow-y: scroll; overflow-x: hidden; }
|
||||
.modal-results li { -webkit-user-select: text !important; -ms-user-select: text !important; user-select: text !important; }
|
||||
.modal-name-big { font-size: 20px; }
|
||||
.modal-name { color: #1447e6; font-weight: bold; font-family: Inconsolata; }
|
||||
.modal-args { color: #178236; font-style: italic; font-family: Inconsolata; }
|
||||
.modal-detail { width: 100%; margin: 10px 0px 0px 0px; }
|
||||
.modal-entry { width: 100%; margin: 0px 0px 0px 20px; list-style: none; }
|
||||
.modal-entry-text { min-width: 200px; display: inline-block; }
|
||||
.modal-searchbar { width: calc(100% - 36px); }
|
||||
.modal-close { float: right; width: 32px; }
|
||||
.shortcut-hint { opacity: .7; font-size: 11px; margin-left: 6px; }
|
||||
.nb-search-hit {
|
||||
background: #ffe08a;
|
||||
color: #111111;
|
||||
border-radius: 2px;
|
||||
padding: 0 1px;
|
||||
}
|
||||
@keyframes nb-search-pulse {
|
||||
0% { box-shadow: 0 0 0 0 rgba(20, 71, 230, 0.18); }
|
||||
100% { box-shadow: 0 0 0 8px rgba(20, 71, 230, 0); }
|
||||
}
|
||||
.nb-search-active-cell {
|
||||
box-shadow: 0 0 0 2px rgba(20, 71, 230, 0.24);
|
||||
border-radius: 8px;
|
||||
animation: nb-search-pulse 0.55s ease-out 1;
|
||||
}
|
||||
.nb-search-toolbar-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.nb-search-toolbar-btn.is-active {
|
||||
color: #1447e6;
|
||||
border-color: rgba(20, 71, 230, 0.35);
|
||||
background: rgba(20, 71, 230, 0.08);
|
||||
}
|
||||
.nb-search-panel {
|
||||
position: fixed;
|
||||
top: 72px;
|
||||
right: 24px;
|
||||
width: min(460px, calc(100vw - 24px));
|
||||
max-height: calc(100vh - 96px);
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
background: linear-gradient(180deg, #fffdf7 0%, #ffffff 62%);
|
||||
border: 1px solid rgba(20, 71, 230, 0.16);
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 22px 50px rgba(17, 24, 39, 0.18);
|
||||
overflow: hidden;
|
||||
z-index: 1000;
|
||||
}
|
||||
.nb-search-panel.is-open { display: flex; }
|
||||
.nb-search-head {
|
||||
padding: 14px 14px 10px 14px;
|
||||
background: linear-gradient(180deg, rgba(20, 71, 230, 0.08) 0%, rgba(255, 224, 138, 0.14) 100%);
|
||||
border-bottom: 1px solid rgba(20, 71, 230, 0.1);
|
||||
}
|
||||
.nb-search-head-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.nb-search-title {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
.nb-search-close {
|
||||
margin-left: auto;
|
||||
border: 0;
|
||||
background: rgba(17, 24, 39, 0.06);
|
||||
color: #111827;
|
||||
border-radius: 999px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.nb-search-input-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 10px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.nb-search-query {
|
||||
font-family: Inconsolata, monospace;
|
||||
}
|
||||
.nb-search-select {
|
||||
font-size: 12px;
|
||||
}
|
||||
.nb-search-mode-row,
|
||||
.nb-search-filter-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.nb-search-mode-label,
|
||||
.nb-search-filter-label {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
color: rgba(17, 24, 39, 0.66);
|
||||
}
|
||||
.nb-search-toggle-group {
|
||||
display: inline-flex;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.nb-search-toggle {
|
||||
border: 1px solid rgba(20, 71, 230, 0.16);
|
||||
background: rgba(255, 255, 255, 0.86);
|
||||
color: #334155;
|
||||
border-radius: 999px;
|
||||
padding: 5px 11px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.nb-search-toggle.is-active {
|
||||
background: #1447e6;
|
||||
border-color: #1447e6;
|
||||
color: #ffffff;
|
||||
}
|
||||
.nb-search-toggle.is-case {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.nb-search-toggle input {
|
||||
margin: 0;
|
||||
}
|
||||
.nb-search-status {
|
||||
padding: 10px 14px;
|
||||
font-size: 12px;
|
||||
color: rgba(17, 24, 39, 0.72);
|
||||
border-bottom: 1px solid rgba(15, 23, 42, 0.08);
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
}
|
||||
.nb-search-status.is-error {
|
||||
color: #b42318;
|
||||
background: rgba(254, 242, 242, 0.92);
|
||||
}
|
||||
.nb-search-results {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 8px;
|
||||
overflow-y: auto;
|
||||
min-height: 140px;
|
||||
}
|
||||
.nb-search-result-item {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.nb-search-result {
|
||||
width: 100%;
|
||||
border: 1px solid transparent;
|
||||
background: transparent;
|
||||
text-align: left;
|
||||
padding: 10px 12px;
|
||||
cursor: pointer;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.nb-search-result:hover,
|
||||
.nb-search-result.is-active {
|
||||
background: rgba(20, 71, 230, 0.08);
|
||||
border-color: rgba(20, 71, 230, 0.16);
|
||||
}
|
||||
.nb-search-result-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
margin-bottom: 6px;
|
||||
font-size: 11px;
|
||||
font-family: Inconsolata, monospace;
|
||||
}
|
||||
.nb-search-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
background: rgba(20, 71, 230, 0.08);
|
||||
color: #1447e6;
|
||||
}
|
||||
.nb-search-badge.is-output {
|
||||
background: rgba(232, 86, 0, 0.12);
|
||||
color: #b54708;
|
||||
}
|
||||
.nb-search-badge.is-fuzzy {
|
||||
background: rgba(23, 130, 54, 0.12);
|
||||
color: #178236;
|
||||
}
|
||||
.nb-search-result-line {
|
||||
font-family: Inconsolata, monospace;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
color: #0f172a;
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
.nb-search-panel {
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
left: 12px;
|
||||
width: auto;
|
||||
max-height: calc(100vh - 24px);
|
||||
}
|
||||
window.scrollTo(0, document.body.scrollHeight);
|
||||
}
|
||||
</style>
|
||||
<script>window.NOTEBOOK_KB={{ keybindings }};</script>
|
||||
<script src="{{ $root }}static/shortcuts.js"></script>
|
||||
<script>
|
||||
window.NB_NOTEBOOK_SEARCH = {
|
||||
searchEndpoint: "{{ $root }}api/v1/json/pages/{{ $page.unique }}/search",
|
||||
markdownEditBase: "{{ $root }}markdown/edit/{{ $page.unique }}/",
|
||||
pageId: "{{ $page.unique }}",
|
||||
storageKey: "nb:notebook-search:{{ $page.unique }}",
|
||||
config: {{ search_config }}
|
||||
};
|
||||
</script>
|
||||
<script src="{{ $root }}static/notebook-search.js"></script>
|
||||
<script>
|
||||
function resizeIframe(obj, padding) {
|
||||
try {
|
||||
var h = obj.contentWindow.document.documentElement.scrollHeight;
|
||||
if (h > 20) { obj.style.height = (h + padding) + 'px'; }
|
||||
} catch(e) {}
|
||||
}
|
||||
function handle(obj) {
|
||||
resizeIframe(obj, 20);
|
||||
try {
|
||||
if (obj.contentWindow.location.href.indexOf("/deleted") > 0) {
|
||||
var group = obj.closest('.cell-group');
|
||||
if (group) { group.remove(); } else { obj.parentNode.remove(); }
|
||||
return;
|
||||
}
|
||||
} catch(e) {}
|
||||
setTimeout(function() { resizeIframe(obj, 20); }, 150);
|
||||
setTimeout(function() { resizeIframe(obj, 20); }, 500);
|
||||
}
|
||||
function output(obj) {
|
||||
obj.style.height = obj.contentWindow.document.documentElement.scrollHeight + 'px';
|
||||
if (obj.contentWindow.location.href.indexOf("/deleted") > 0) {
|
||||
obj.parentNode.parentNode.removeChild(obj.parentNode);
|
||||
} else if (obj.contentWindow.location.href.indexOf("/loaded") > 0) {
|
||||
location.reload()
|
||||
resizeIframe(obj, 10);
|
||||
try {
|
||||
var loc = obj.contentWindow.location.href;
|
||||
if (loc.indexOf("/deleted") > 0) {
|
||||
var group = obj.closest('.cell-group');
|
||||
if (group) { group.remove(); } else { obj.parentNode.remove(); }
|
||||
return;
|
||||
}
|
||||
if (loc.indexOf("/loaded") > 0) {
|
||||
location.reload();
|
||||
return;
|
||||
}
|
||||
} catch(e) {}
|
||||
setTimeout(function() { resizeIframe(obj, 10); }, 150);
|
||||
setTimeout(function() { resizeIframe(obj, 10); }, 500);
|
||||
}
|
||||
window.addEventListener('message', function(e) {
|
||||
if (!e.data || !e.data.type) return;
|
||||
if (e.data.type === 'iframe-resize') {
|
||||
var iframes = document.querySelectorAll('iframe');
|
||||
for (var k = 0; k < iframes.length; k++) {
|
||||
try {
|
||||
if (iframes[k].contentWindow === e.source) {
|
||||
iframes[k].style.height = Math.max(e.data.height, 50) + 'px';
|
||||
break;
|
||||
}
|
||||
} catch(err) {}
|
||||
}
|
||||
} else if (e.data.type === 'iframe-remove') {
|
||||
var iframes = document.querySelectorAll('iframe');
|
||||
for (var k = 0; k < iframes.length; k++) {
|
||||
try {
|
||||
if (iframes[k].contentWindow === e.source) {
|
||||
var group = iframes[k].closest('.cell-group');
|
||||
if (group) { group.remove(); } else { iframes[k].parentNode.remove(); }
|
||||
break;
|
||||
}
|
||||
} catch(err) {}
|
||||
}
|
||||
} else if (e.data.type === 'iframe-reload') {
|
||||
location.reload();
|
||||
} else if (e.data.type === 'iframe-add-cell') {
|
||||
var iframes = document.querySelectorAll('iframe');
|
||||
for (var k = 0; k < iframes.length; k++) {
|
||||
try {
|
||||
if (iframes[k].contentWindow === e.source) {
|
||||
appendOutputCell(iframes[k], e.data);
|
||||
break;
|
||||
}
|
||||
} catch(err) {}
|
||||
}
|
||||
}
|
||||
window.scrollTo(0, document.body.scrollHeight);
|
||||
});
|
||||
function appendOutputCell(sourceIframe, data) {
|
||||
if (!data || !data.cellUnique || !data.outputUrl) return;
|
||||
if (document.getElementById('cell-' + data.cellUnique)) return;
|
||||
|
||||
var body = document.getElementById('page-body');
|
||||
if (!body) return;
|
||||
|
||||
var group = document.createElement('div');
|
||||
group.className = 'cell-group';
|
||||
group.id = 'cell-' + data.cellUnique;
|
||||
|
||||
var inputGroup = document.createElement('div');
|
||||
inputGroup.className = 'input-group spacing-top';
|
||||
|
||||
var code = document.createElement('code');
|
||||
code.className = 'input-group-addon spacing-right spacing-left nb-searchable-code';
|
||||
code.id = 'content-' + data.cellUnique;
|
||||
code.setAttribute('data-cell-id', data.cellUnique);
|
||||
code.setAttribute('data-cell-type', data.cellType || '');
|
||||
if (data.cellType === 'script') {
|
||||
code.style.display = 'block';
|
||||
code.style.whiteSpace = 'pre-wrap';
|
||||
}
|
||||
code.textContent = data.content || '';
|
||||
inputGroup.appendChild(code);
|
||||
|
||||
var button = document.createElement('button');
|
||||
button.className = 'btn float-right';
|
||||
button.type = 'button';
|
||||
var icon = document.createElement('i');
|
||||
icon.className = 'icon icon-delete icon-height';
|
||||
icon.textContent = 'Delete';
|
||||
button.appendChild(icon);
|
||||
button.onclick = function() { deleteCell(data.deleteUrl, data.cellUnique); };
|
||||
inputGroup.appendChild(button);
|
||||
|
||||
var outputGroup = document.createElement('div');
|
||||
outputGroup.className = 'input-group spacing-top';
|
||||
|
||||
var iframe = document.createElement('iframe');
|
||||
iframe.id = 'output-' + data.cellUnique;
|
||||
iframe.setAttribute('data-cell-id', data.cellUnique);
|
||||
iframe.setAttribute('frameborder', '0');
|
||||
iframe.setAttribute('marginheight', '0');
|
||||
iframe.setAttribute('marginwidth', '0');
|
||||
iframe.setAttribute('width', '100%');
|
||||
iframe.setAttribute('scrolling', 'no');
|
||||
iframe.setAttribute('onload', 'handle(this)');
|
||||
iframe.className = 'input-group-addon spacing-left md-heigh';
|
||||
iframe.src = data.outputUrl;
|
||||
outputGroup.appendChild(iframe);
|
||||
|
||||
var footer = document.createElement('div');
|
||||
footer.className = 'cell-footer';
|
||||
var footerCount = 0;
|
||||
function appendFooterMetric(label, value) {
|
||||
if (!value) return;
|
||||
var span = document.createElement('span');
|
||||
span.textContent = label + ': ' + value;
|
||||
footer.appendChild(span);
|
||||
footerCount++;
|
||||
}
|
||||
appendFooterMetric('lines', data.lineCount);
|
||||
appendFooterMetric('words', data.wordCount);
|
||||
appendFooterMetric('time', data.actionTime);
|
||||
|
||||
group.appendChild(inputGroup);
|
||||
group.appendChild(outputGroup);
|
||||
if (footerCount > 0) {
|
||||
group.appendChild(footer);
|
||||
}
|
||||
|
||||
var anchor = sourceIframe.closest('.panel');
|
||||
if (anchor && anchor.parentNode === body) {
|
||||
body.insertBefore(group, anchor);
|
||||
} else {
|
||||
body.insertBefore(group, body.childNodes[body.childNodes.length - 2]);
|
||||
}
|
||||
}
|
||||
function deleteCell(url, cellId) {
|
||||
if (!window.confirm("Are you sure you want to delete this cell?")) return;
|
||||
fetch(url, {redirect: 'follow'}).then(function() {
|
||||
var el = document.getElementById('cell-' + cellId);
|
||||
if (el) el.remove();
|
||||
});
|
||||
}
|
||||
function isCommandHelpOpen() {
|
||||
var modal = document.getElementById('command-help');
|
||||
return modal && modal.className.indexOf(' active') >= 0;
|
||||
}
|
||||
function newmd() {
|
||||
var body = document.getElementById("page-body");
|
||||
@@ -49,14 +437,32 @@
|
||||
iframe.setAttribute("frameborder", "0");
|
||||
iframe.setAttribute("marginheight", "0");
|
||||
iframe.setAttribute("marginwidth", "0");
|
||||
iframe.style.height = "300px";
|
||||
iframe.setAttribute("width", "100%");
|
||||
iframe.setAttribute("height", "100%");
|
||||
iframe.setAttribute("scrolling", "no");
|
||||
iframe.setAttribute("onload", "handle(this)");
|
||||
iframe.src = "{{ $root }}markdown/new/{{ $page.unique }}";
|
||||
div.appendChild(iframe);
|
||||
body.insertBefore(div, body.childNodes[body.childNodes.length - 2]);
|
||||
}
|
||||
function newsc() {
|
||||
var body = document.getElementById("page-body");
|
||||
var div = document.createElement("div");
|
||||
var iframe = document.createElement("iframe");
|
||||
|
||||
div.className = "panel spacing-top";
|
||||
iframe.className = "panel-body no-borders";
|
||||
iframe.style.height = "300px";
|
||||
iframe.setAttribute("frameborder", "0");
|
||||
iframe.setAttribute("marginheight", "0");
|
||||
iframe.setAttribute("marginwidth", "0");
|
||||
iframe.setAttribute("width", "100%");
|
||||
iframe.setAttribute("scrolling", "no");
|
||||
iframe.setAttribute("onload", "output(this)");
|
||||
iframe.src = "{{ $root }}output/input/script/{{ $page.unique }}";
|
||||
div.appendChild(iframe);
|
||||
body.insertBefore(div, body.childNodes[body.childNodes.length - 2]);
|
||||
}
|
||||
function newcm() {
|
||||
var body = document.getElementById("page-body");
|
||||
var div = document.createElement("div");
|
||||
@@ -64,14 +470,14 @@
|
||||
|
||||
div.className = "panel spacing-top";
|
||||
iframe.className = "panel-body no-borders";
|
||||
iframe.style.height = "120px";
|
||||
iframe.setAttribute("frameborder", "0");
|
||||
iframe.setAttribute("marginheight", "0");
|
||||
iframe.setAttribute("marginwidth", "0");
|
||||
iframe.setAttribute("width", "100%");
|
||||
iframe.setAttribute("height", "100%");
|
||||
iframe.setAttribute("scrolling", "no");
|
||||
iframe.setAttribute("onload", "output(this)");
|
||||
iframe.src = "{{ $root }}output/input/{{ $page.unique }}";
|
||||
iframe.src = "{{ $root }}output/input/console/{{ $page.unique }}";
|
||||
div.appendChild(iframe);
|
||||
body.insertBefore(div, body.childNodes[body.childNodes.length - 2]);
|
||||
}
|
||||
@@ -80,6 +486,136 @@
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
{{ if $pipe }}
|
||||
var rizinCmds = JSON.parse({{ .cmds | stringify }});
|
||||
var rizinCmdsList = Object.keys(rizinCmds);
|
||||
function openCmdHelp() {
|
||||
var modal = document.getElementById("command-help");
|
||||
modal.className += " active";
|
||||
var search = document.getElementById("search-focus");
|
||||
search.focus();
|
||||
}
|
||||
function closeCmdHelp() {
|
||||
var modal = document.getElementById("command-help");
|
||||
modal.className = modal.className.replace(" active", "");
|
||||
}
|
||||
function cmdAsText(parent, command) {
|
||||
var details = (command.details || []);
|
||||
var element = document.createElement('span');
|
||||
element.className = "modal-name modal-name-big";
|
||||
element.textContent = command.cmd;
|
||||
parent.appendChild(element);
|
||||
|
||||
element = document.createElement('span');
|
||||
element.className = "modal-args modal-name-big";
|
||||
element.textContent = " " + command.args_str;
|
||||
parent.appendChild(element);
|
||||
|
||||
if ((command.summary || "") != "") {
|
||||
element = document.createElement('div');
|
||||
element.textContent = command.summary.replace(".", ".\n");
|
||||
parent.appendChild(element);
|
||||
}
|
||||
if ((command.description || "") != "") {
|
||||
element = document.createElement('div');
|
||||
element.textContent = command.description.replace(".", ".\n");
|
||||
parent.appendChild(element);
|
||||
}
|
||||
for (var i = 0; i < details.length; i++) {
|
||||
var entries = (details[i].entries || []);
|
||||
var detail = document.createElement('ul');
|
||||
detail.className = "modal-detail";
|
||||
detail.textContent = details[i].name;
|
||||
parent.appendChild(detail);
|
||||
for (var j = 0; j < entries.length; j++) {
|
||||
var entry = document.createElement('li');
|
||||
entry.className = "modal-entry";
|
||||
detail.appendChild(entry);
|
||||
|
||||
var text = document.createElement('span');
|
||||
text.className = "modal-entry-text";
|
||||
entry.appendChild(text);
|
||||
|
||||
if ((entries[j].text || "").length > 0) {
|
||||
element = document.createElement('span');
|
||||
element.className = "modal-name";
|
||||
element.textContent = entries[j].text;
|
||||
text.appendChild(element);
|
||||
}
|
||||
|
||||
if ((entries[j].arg_str || "").length > 0) {
|
||||
element = document.createElement('span');
|
||||
element.className = "modal-args";
|
||||
element.textContent = entries[j].arg_str;
|
||||
text.appendChild(element);
|
||||
}
|
||||
|
||||
if ((entries[j].comment || "").length > 0) {
|
||||
element = document.createElement('span');
|
||||
element.style.display = "inline-block";
|
||||
element.textContent = " # " + entries[j].comment;
|
||||
entry.appendChild(element);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
function findCmd(element) {
|
||||
var dropdown = document.getElementById("dropdown-results");
|
||||
var results = document.getElementById("search-results");
|
||||
results.replaceChildren();
|
||||
var tokens = element.value.split(/\s+/);
|
||||
for (var i = 0, count = 0; i < rizinCmdsList.length; i++) {
|
||||
var key = rizinCmdsList[i];
|
||||
var descr = rizinCmds[key].description || "";
|
||||
var summr = rizinCmds[key].summary || "";
|
||||
var found = true;
|
||||
for (var j = 0; j < tokens.length; j++) {
|
||||
if (key.indexOf(tokens[j]) < 0 && descr.indexOf(tokens[j]) < 0 && summr.indexOf(tokens[j]) < 0) {
|
||||
found = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
continue;
|
||||
}
|
||||
var li = document.createElement('li');
|
||||
if (count > 0) {
|
||||
li.className = "divider";
|
||||
results.appendChild(li);
|
||||
li = document.createElement('li');
|
||||
}
|
||||
li.className = "menu-item";
|
||||
cmdAsText(li, rizinCmds[key]);
|
||||
results.appendChild(li);
|
||||
count++;
|
||||
}
|
||||
if (dropdown.className.indexOf(" active") < 0) {
|
||||
dropdown.className += " active";
|
||||
}
|
||||
}
|
||||
{{ end }}
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
NotebookShortcuts.annotate(document.getElementById('nav-about'), 'about');
|
||||
NotebookShortcuts.annotate(document.getElementById('nav-settings'), 'settings');
|
||||
NotebookShortcuts.annotate(document.getElementById('nav-new-page'), 'new_page');
|
||||
NotebookShortcuts.annotate(document.getElementById('toggle-pipe'), 'toggle_pipe');
|
||||
NotebookShortcuts.annotate(document.getElementById('open-notebook-search'), 'search_notebook');
|
||||
{{ if $pipe }}NotebookShortcuts.annotate(document.getElementById('open-command-line'), 'new_command');{{ end }}
|
||||
NotebookShortcuts.annotate(document.getElementById('open-markdown'), 'new_markdown');
|
||||
NotebookShortcuts.annotate(document.getElementById('open-script'), 'new_script');
|
||||
NotebookShortcuts.bind([
|
||||
{ action: 'about', handler: function() { document.getElementById('nav-about').click(); } },
|
||||
{ action: 'new_page', handler: function() { document.getElementById('nav-new-page').click(); } },
|
||||
{ action: 'settings', handler: function() { document.getElementById('nav-settings').click(); } },
|
||||
{ action: 'toggle_pipe', handler: function() { document.getElementById('toggle-pipe').click(); } },
|
||||
{ action: 'search_notebook', when: function() { return !isCommandHelpOpen(); }, handler: function() { openNotebookSearch(); } },
|
||||
{ action: 'new_markdown', when: function() { return !isCommandHelpOpen(); }, handler: function() { newmd(); } },
|
||||
{ action: 'new_script', when: function() { return !isCommandHelpOpen(); }, handler: function() { newsc(); } },
|
||||
{ action: 'new_command', when: function() { return !isCommandHelpOpen() && {{ if $pipe }}true{{ else }}false{{ end }}; }, handler: function() { newcm(); } },
|
||||
{ action: 'cancel', when: function() { return isNotebookSearchOpen(); }, handler: function() { closeNotebookSearch(); } },
|
||||
{ action: 'cancel', when: function() { return isCommandHelpOpen(); }, handler: function() { closeCmdHelp(); } }
|
||||
]);
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
@@ -92,10 +628,27 @@
|
||||
<section class="navbar-section">
|
||||
<h4 class="title-margin">{{ $page.title }}</h4>
|
||||
|
||||
<a href="{{ $root }}pipe/{{ if $pipe }}close{{ else }}open{{ end }}/{{ $page.unique }}" class="btn{{ if $pipe }} pipe{{ end }}">
|
||||
<a href="{{ $root }}about" class="btn" id="nav-about">About</a>
|
||||
|
||||
<a href="{{ $root }}settings" class="btn" id="nav-settings">Settings</a>
|
||||
|
||||
<a href="{{ $root }}new" class="btn" id="nav-new-page">New Page</a>
|
||||
|
||||
{{ if not $page.hasBinary }}
|
||||
<a href="{{ $root }}edit/{{ $page.unique }}" class="btn btn-error">Attach Binary</a>
|
||||
|
||||
{{ end }}
|
||||
<a href="{{ $root }}pipe/{{ if $pipe }}close{{ else }}open{{ end }}/{{ $page.unique }}" class="btn{{ if $pipe }} pipe{{ end }}" id="toggle-pipe">
|
||||
{{ if $pipe }}Close Pipe{{ else }}Open Pipe{{ end }}
|
||||
</a>
|
||||
|
||||
<a href="{{ $root }}api/v1/json/pages/{{ $page.unique }}/export" class="btn" download>Export .rznb</a>
|
||||
|
||||
<button class="btn nb-search-toolbar-btn" type="button" id="open-notebook-search">
|
||||
<i class="icon icon-search"></i>
|
||||
<span>Search</span>
|
||||
</button>
|
||||
|
||||
<a href="{{ $root }}" class="btn btn-primary">Go Back</a>
|
||||
</section>
|
||||
</header>
|
||||
@@ -103,8 +656,13 @@
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<div class="input-group spacing-top">
|
||||
<code class="input-group-addon spacing-right">File: {{ $page.filename }}</code>
|
||||
<code class="input-group-addon spacing-right">File: {{ if $page.filename }}{{ $page.filename }}{{ else }}none attached{{ end }}</code>
|
||||
</div>
|
||||
{{ if $page.binaryHash }}
|
||||
<div class="input-group spacing-top">
|
||||
<code class="input-group-addon spacing-right" style="font-size: 11px;">SHA-256: {{ $page.binaryHash }}</code>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -115,31 +673,111 @@
|
||||
<div class="column" style="padding-top: 0px" id="page-body">
|
||||
{{ range $i, $line := $page.lines }}
|
||||
{{ if eq $line.type "markdown" }}
|
||||
<div class="cell-group" id="cell-{{ $line.unique }}">
|
||||
<div class="panel spacing-top resizer">
|
||||
<iframe frameborder="0" marginheight="0" marginwidth="0" width="100%" height="100%" scrolling="no" onload="handle(this)" onunload="handle(this)" class="panel-body no-borders md-heigh" src="{{ $root }}markdown/view/{{ $page.unique }}/{{ $line.unique }}"></iframe>
|
||||
<iframe id="markdown-{{ $line.unique }}" data-cell-id="{{ $line.unique }}" frameborder="0" marginheight="0" marginwidth="0" width="100%" scrolling="no" onload="handle(this)" class="panel-body no-borders md-heigh" src="{{ $root }}markdown/view/{{ $page.unique }}/{{ $line.unique }}"></iframe>
|
||||
</div>
|
||||
{{ if or $line.lineCount $line.wordCount $line.actionTime }}
|
||||
<div class="cell-footer">
|
||||
{{ if $line.lineCount }}<span>lines: {{ $line.lineCount }}</span>{{ end }}
|
||||
{{ if $line.wordCount }}<span>words: {{ $line.wordCount }}</span>{{ end }}
|
||||
{{ if $line.actionTime }}<span>time: {{ $line.actionTime }}</span>{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ else }}
|
||||
<div class="cell-group" id="cell-{{ $line.unique }}">
|
||||
<div class="input-group spacing-top">
|
||||
<code class="input-group-addon spacing-right spacing-left">{{ $line.command }}</code>
|
||||
<a class="btn float-right" href="{{ $root }}output/delete/{{ $page.unique }}/{{ $line.unique }}" onclick="confirm_delete(event)">
|
||||
{{ if eq $line.type "script" }}
|
||||
<code id="content-{{ $line.unique }}" data-cell-id="{{ $line.unique }}" data-cell-type="{{ $line.type }}" style="display: block; white-space: pre-wrap;" class="input-group-addon spacing-right spacing-left nb-searchable-code">{{ $line.script }}</code>
|
||||
{{ else }}
|
||||
<code id="content-{{ $line.unique }}" data-cell-id="{{ $line.unique }}" data-cell-type="{{ $line.type }}" class="input-group-addon spacing-right spacing-left nb-searchable-code">{{ $line.command }}</code>
|
||||
{{ end }}
|
||||
<button class="btn float-right" type="button" onclick="deleteCell('{{ $root }}output/delete/{{ $page.unique }}/{{ $line.unique }}', '{{ $line.unique }}')">
|
||||
<i class="icon icon-delete icon-height">Delete</i>
|
||||
</a>
|
||||
</button>
|
||||
</div>
|
||||
<div class="input-group spacing-top">
|
||||
<iframe id="output-{{ $i }}" frameborder="0" marginheight="0" marginwidth="0" width="100%" height="100%" scrolling="no" onload="handle(this)" class="input-group-addon spacing-left md-heigh" src="{{ $root }}output/view/{{ $page.unique }}/{{ $line.unique }}"></iframe>
|
||||
<iframe id="output-{{ $line.unique }}" data-cell-id="{{ $line.unique }}" frameborder="0" marginheight="0" marginwidth="0" width="100%" scrolling="no" onload="handle(this)" class="input-group-addon spacing-left md-heigh" src="{{ $root }}output/view/{{ $page.unique }}/{{ $line.unique }}"></iframe>
|
||||
</div>
|
||||
{{ if or $line.lineCount $line.wordCount $line.actionTime }}
|
||||
<div class="cell-footer">
|
||||
{{ if $line.lineCount }}<span>lines: {{ $line.lineCount }}</span>{{ end }}
|
||||
{{ if $line.wordCount }}<span>words: {{ $line.wordCount }}</span>{{ end }}
|
||||
{{ if $line.actionTime }}<span>time: {{ $line.actionTime }}</span>{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
<div class="input-group spacing-top" style="margin-top: 10px;">
|
||||
<a class="btn" href="#" onclick="newmd()"><i class="icon icon-message">Markdown</i> Markdown</a>
|
||||
<a class="btn" href="#" onclick="newmd()" id="open-markdown"><i class="icon icon-message">Markdown</i> Markdown</a>
|
||||
|
||||
<a class="btn" href="#" onclick="newsc()" id="open-script"><i class="icon icon-share">Script</i> Script</a>
|
||||
{{ if $pipe }}
|
||||
|
||||
<a class="btn" href="#" onclick="newcm()"><i class="icon icon-resize-horiz">Command Line</i> Command Line</a>
|
||||
<a class="btn" href="#" onclick="newcm()" id="open-command-line"><i class="icon icon-resize-horiz">Command Line</i> Command Line</a>
|
||||
|
||||
<a class="btn" href="#" onclick="openCmdHelp()"><i class="icon icon-search">Help</i> Help</a>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<aside id="notebook-search" class="nb-search-panel" aria-hidden="true">
|
||||
<div class="nb-search-head">
|
||||
<div class="nb-search-head-top">
|
||||
<h3 class="nb-search-title">Notebook Search</h3>
|
||||
<button class="nb-search-close" type="button" id="close-notebook-search" aria-label="Close notebook search">×</button>
|
||||
</div>
|
||||
<div class="nb-search-input-row">
|
||||
<input class="form-input nb-search-query" id="notebook-search-focus" value="" placeholder="Search commands, scripts, outputs, and markdown"/>
|
||||
<select class="form-select nb-search-select" id="notebook-search-surface" aria-label="Search surface">
|
||||
<option value="all">All surfaces</option>
|
||||
<option value="content">Input / source</option>
|
||||
<option value="output">Outputs</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="nb-search-mode-row">
|
||||
<span class="nb-search-mode-label">Mode</span>
|
||||
<div class="nb-search-toggle-group" id="notebook-search-mode-group">
|
||||
<button class="nb-search-toggle" type="button" data-mode="literal">Literal</button>
|
||||
<button class="nb-search-toggle" type="button" data-mode="regex">Regex</button>
|
||||
<button class="nb-search-toggle" type="button" data-mode="fuzzy">Fuzzy</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nb-search-filter-row">
|
||||
<span class="nb-search-filter-label">Filters</span>
|
||||
<select class="form-select nb-search-select" id="notebook-search-cell-type" aria-label="Cell type filter">
|
||||
<option value="all">All cell types</option>
|
||||
<option value="command">Command cells</option>
|
||||
<option value="script">Script cells</option>
|
||||
<option value="markdown">Markdown cells</option>
|
||||
</select>
|
||||
<label class="nb-search-toggle is-case" for="notebook-search-case">
|
||||
<input type="checkbox" id="notebook-search-case" />
|
||||
<span>Case sensitive</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nb-search-status" id="notebook-search-status">Search commands, scripts, outputs, and markdown in this notebook.</div>
|
||||
<ul class="nb-search-results" id="notebook-search-results"></ul>
|
||||
</aside>
|
||||
{{ if $pipe }}
|
||||
<div id="command-help" class="modal modal-box">
|
||||
<div class="modal-content modal-search" onclick="" style="z-index: 999999;">
|
||||
<div class="modal-body">
|
||||
<div class="content">
|
||||
<div class="dropdown" id="dropdown-results" style="width: 100%; height: 100%; max-height: 500px;">
|
||||
<button class="btn btn-primary modal-close" onclick="closeCmdHelp()">×</button>
|
||||
<input class="form-input modal-searchbar" id="search-focus" onchange="findCmd(this);" onkeypress="findCmd(this);" onpaste="findCmd(this);" oninput="findCmd(this);" value="" placeholder="Search for a command or keyword in a description"/>
|
||||
<ul class="modal-results menu active" id="search-results"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
</section>
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
@@ -21,11 +21,11 @@
|
||||
border-top-color:transparent;
|
||||
}
|
||||
</style>
|
||||
<meta http-equiv="refresh" content="2"/>
|
||||
<meta http-equiv="refresh" content="1"/>
|
||||
</head>
|
||||
<body>
|
||||
<div class="center">
|
||||
<div class="loading-xl loading"></div> Waiting for rizin output. Please wait.
|
||||
<div class="loading-xl loading"></div> Reloading page...
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,95 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Rizin Notebook</title>
|
||||
<link rel="stylesheet" href="{{ .root }}static/spectre-icons.min.css">
|
||||
<link rel="stylesheet" href="{{ .root }}static/spectre.min.css">
|
||||
<link rel="stylesheet" href="{{ .root }}static/simplemde.min.css">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<style type="text/css">
|
||||
html, body { width: 100%; height: 100%; margin: 0 }
|
||||
.button-edit { width: 75px ! important; margin-top: 9px; margin-right: 9px; }
|
||||
@font-face { font-family: 'Inconsolata'; src: URL('{{ .root }}static/inconsolata.ttf') format('truetype'); }
|
||||
</style>
|
||||
<link rel="stylesheet" href="{{ .root }}static/codelines.css">
|
||||
<script>window.NOTEBOOK_KB={{ keybindings }};</script>
|
||||
<script src="{{ .root }}static/shortcuts.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
{{ if .message }}
|
||||
<div class="toast toast-success" style="margin: 12px 12px 0 12px;">{{ .message }}</div>
|
||||
{{ end }}
|
||||
{{ if .error }}
|
||||
<div class="toast toast-error" style="margin: 12px 12px 0 12px;">{{ .error }}</div>
|
||||
{{ end }}
|
||||
<form style="margin-top: 15px; height: 100%;" action="{{ .root }}output/script/{{ .unique }}" method="POST">
|
||||
<div class="form-group">
|
||||
<p><textarea id="codelines" class="code-lines" wrap="off" readonly>1 </textarea><textarea id="codeeditor" class="code-editor" wrap="off" name="script">{{ if .script }}{{ .script }}{{ else }}console.log(rizin.cmd("?e hello world").trim(), "from the script");{{ end }}</textarea></p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button class="btn btn-primary" type="submit">Run</button>
|
||||
<button class="btn" type="button" id="cancel-script" onclick="window.parent.postMessage({type:'iframe-remove'},'*')">Cancel</button>
|
||||
<span style="font-size:12px;margin-left:8px;opacity:.7;">Ctrl+Enter to run</span>
|
||||
</div>
|
||||
</form>
|
||||
<script type="text/javascript">
|
||||
var createdCellUnique = {{ if .cellUnique }}"{{ .cellUnique }}"{{ else }}""{{ end }};
|
||||
var createdCellLineCount = {{ if .lineCount }}{{ .lineCount }}{{ else }}0{{ end }};
|
||||
var createdCellWordCount = {{ if .wordCount }}{{ .wordCount }}{{ else }}0{{ end }};
|
||||
var createdCellActionTime = {{ if .actionTime }}"{{ .actionTime }}"{{ else }}""{{ end }};
|
||||
var editor = document.getElementById('codeeditor');
|
||||
var lines = document.getElementById('codelines');
|
||||
NotebookShortcuts.annotate(document.getElementById('cancel-script'), 'cancel');
|
||||
NotebookShortcuts.bind([
|
||||
{ action: 'cancel', allowInInputs: true, handler: function() { document.getElementById('cancel-script').click(); } },
|
||||
{ action: 'execute', allowInInputs: true, handler: function() { editor.form.submit(); } }
|
||||
]);
|
||||
editor.addEventListener('scroll', function() {
|
||||
lines.scrollTop = editor.scrollTop;
|
||||
lines.scrollLeft = editor.scrollLeft;
|
||||
});
|
||||
editor.addEventListener('keydown', function(e) {
|
||||
if (e.keyCode === 9) {
|
||||
// TAB = 9
|
||||
e.preventDefault();
|
||||
editor.value = editor.value.slice(0, editor.selectionStart) + '\t' + editor.value.slice(editor.selectionEnd);
|
||||
editor.setSelectionRange(editor.selectionStart + 2, editor.selectionStart + 2)
|
||||
}
|
||||
});
|
||||
var lineCountCache = 0;
|
||||
function updateLines() {
|
||||
var editorLines = editor.value.split('\n');
|
||||
if (lineCountCache != editorLines.length) {
|
||||
lines.value = editorLines.map(function(e, n) {
|
||||
return (n + 1).toString() + ' ';
|
||||
}).join('\n');
|
||||
}
|
||||
lineCountCache = editorLines.length;
|
||||
}
|
||||
editor.addEventListener('input', function() {
|
||||
updateLines();
|
||||
});
|
||||
editor.addEventListener('keydown', function(e) {
|
||||
if (e.ctrlKey && e.key === 'Enter') { e.preventDefault(); editor.form.submit(); }
|
||||
});
|
||||
(function() {
|
||||
updateLines();
|
||||
if (createdCellUnique && window.parent !== window) {
|
||||
window.parent.postMessage({
|
||||
type:'iframe-add-cell',
|
||||
cellType:'script',
|
||||
cellUnique:createdCellUnique,
|
||||
content:editor.value,
|
||||
lineCount:createdCellLineCount,
|
||||
wordCount:createdCellWordCount,
|
||||
actionTime:createdCellActionTime,
|
||||
outputUrl:'{{ .root }}output/view/{{ .unique }}/' + createdCellUnique,
|
||||
deleteUrl:'{{ .root }}output/delete/{{ .unique }}/' + createdCellUnique
|
||||
}, '*');
|
||||
}
|
||||
editor.focus();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -7,18 +7,21 @@
|
||||
<link rel="stylesheet" href="{{ $root }}static/spectre.min.css">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<style type="text/css">
|
||||
.spacing-top { margin: 3px 0px 0px 0px; }
|
||||
.spacing-left { margin: 0px 3px 0px 0px; }
|
||||
.spacing-top { margin: 3px 0px 0px 0px !important; }
|
||||
.spacing-left { margin: 0px 3px 0px 0px !important; }
|
||||
.navbar-fixed-height { height: 32px; }
|
||||
.inline { display: inline; }
|
||||
.icon-height { height: 20px; }
|
||||
iframe { border: 0px; width: 100%; height: 100% }
|
||||
</style>
|
||||
<script>window.NOTEBOOK_KB={{ keybindings }};</script>
|
||||
<script src="{{ $root }}static/shortcuts.js"></script>
|
||||
<script type="text/javascript">
|
||||
{{ if eq .action "environment" }}
|
||||
function confirm_delete(event) {
|
||||
if (!window.confirm("Are you sure you want to delete it?")) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
document.getElementById('subaction').value = "delete";
|
||||
}
|
||||
@@ -29,7 +32,89 @@
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
{{ else if eq .action "keybinding" }}
|
||||
function verify(event) {
|
||||
var value = document.getElementById('value').value.trim();
|
||||
if (value.length < 1) {
|
||||
alert("Shortcut combo cannot be empty.");
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
function startRecording() {
|
||||
var btn = document.getElementById('record-btn');
|
||||
var input = document.getElementById('value');
|
||||
btn.textContent = 'Press a key combo...';
|
||||
btn.classList.add('btn-error');
|
||||
input.value = '';
|
||||
input.placeholder = 'Press keys now...';
|
||||
NotebookShortcuts.record(function(combo) {
|
||||
input.value = combo;
|
||||
btn.textContent = 'Record';
|
||||
btn.classList.remove('btn-error');
|
||||
input.placeholder = 'e.g. alt+n, ctrl+s, ctrl+enter';
|
||||
});
|
||||
}
|
||||
{{ else if eq .action "autocomplete" }}
|
||||
function verify(event) {
|
||||
var raw = document.getElementById('value').value.trim();
|
||||
var val = parseInt(raw, 10);
|
||||
if (isNaN(val) || raw.length < 1) {
|
||||
alert("Please enter a valid number.");
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (val < 1) {
|
||||
alert("Value must be at least 1.");
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (val > 100) {
|
||||
alert("Value must be at most 100.");
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
}
|
||||
{{ else if eq .action "search" }}
|
||||
function verify(event) {
|
||||
if ("{{ .input_type }}" !== "number") {
|
||||
return;
|
||||
}
|
||||
var raw = document.getElementById('value').value.trim();
|
||||
var val = parseInt(raw, 10);
|
||||
if (isNaN(val) || raw.length < 1) {
|
||||
alert("Please enter a valid number.");
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (val < {{ .min_value }}) {
|
||||
alert("Value must be at least {{ .min_value }}.");
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (val > {{ .max_value }}) {
|
||||
alert("Value must be at most {{ .max_value }}.");
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
}
|
||||
{{ else }}
|
||||
function verify(event) {}
|
||||
{{ end }}
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
NotebookShortcuts.annotate(document.getElementById('nav-home'), 'cancel');
|
||||
NotebookShortcuts.annotate(document.getElementById('nav-about'), 'about');
|
||||
NotebookShortcuts.annotate(document.getElementById('save-settings'), 'save');
|
||||
NotebookShortcuts.bind([
|
||||
{ action: 'about', handler: function() { document.getElementById('nav-about').click(); } },
|
||||
{ action: 'new_page', handler: function() { window.location.href = '{{ $root }}new'; } },
|
||||
{ action: 'save', allowInInputs: true, handler: function() { document.getElementById('save-settings').click(); } },
|
||||
{ action: 'cancel', handler: function() { document.getElementById('nav-home').click(); } }
|
||||
]);
|
||||
var labels = document.querySelectorAll('[data-shortcut-label]');
|
||||
for (var i = 0; i < labels.length; i++) {
|
||||
labels[i].textContent = NotebookShortcuts.formatCombo(labels[i].getAttribute('data-shortcut-label'));
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
@@ -40,7 +125,11 @@
|
||||
<a href="{{ $root }}" class="navbar-brand mr-10">Rizin Notebook</a>
|
||||
</section>
|
||||
<section class="navbar-section">
|
||||
<a href="{{ $root }}about" class="btn" id="nav-about">About</a>
|
||||
|
||||
Edit Settings
|
||||
|
||||
<a href="{{ $root }}settings" class="btn btn-primary" id="nav-home">Go Back</a>
|
||||
</section>
|
||||
</header>
|
||||
<section class="container">
|
||||
@@ -56,10 +145,42 @@
|
||||
<input type="text" class="form-input" name="key" id="key" value="{{ .editkey }}" placeholder="Environment Variable Name"/>
|
||||
<label class="form-label" for="value">Value</label>
|
||||
<input type="text" class="form-input" name="value" id="value" value="{{ if gt $eklen 0 }}{{ index .data .editkey }}{{ end }}" placeholder="Insert a variable value (or leave it empty)"/>
|
||||
{{ else if eq .action "keybinding" }}
|
||||
<input type="hidden" name="editkey" value="{{ .editkey }}" />
|
||||
<input type="hidden" name="subaction" id="subaction" value="save" />
|
||||
<label class="form-label">Action: <strong>{{ .label }}</strong></label>
|
||||
<label class="form-label" for="value">Keyboard Shortcut</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-input" name="value" id="value" value="{{ .combo }}" placeholder="e.g. alt+n, ctrl+s, ctrl+enter" />
|
||||
<button class="btn input-group-btn" type="button" id="record-btn" onclick="startRecording()">Record</button>
|
||||
</div>
|
||||
<p class="form-input-hint">Default: <code data-shortcut-label="{{ .default_combo }}"></code> — Type a combo like <code>alt+n</code> or click Record and press keys.</p>
|
||||
{{ else if eq .action "autocomplete" }}
|
||||
<input type="hidden" name="editkey" value="{{ .editkey }}" />
|
||||
<input type="hidden" name="subaction" id="subaction" value="save" />
|
||||
<label class="form-label">Setting: <strong>{{ .label }}</strong></label>
|
||||
<label class="form-label" for="value">Value</label>
|
||||
<input type="number" class="form-input" name="value" id="value" value="{{ .current_value }}" min="1" max="100" placeholder="Enter a number (1-100)" />
|
||||
<p class="form-input-hint">Default: <code>{{ .default_value }}</code> — Enter a number between 1 and 100.</p>
|
||||
{{ else if eq .action "search" }}
|
||||
<input type="hidden" name="editkey" value="{{ .editkey }}" />
|
||||
<input type="hidden" name="subaction" id="subaction" value="save" />
|
||||
<label class="form-label">Setting: <strong>{{ .label }}</strong></label>
|
||||
<label class="form-label" for="value">Value</label>
|
||||
{{ if eq .input_type "number" }}
|
||||
<input type="number" class="form-input" name="value" id="value" value="{{ .current_value }}" min="{{ .min_value }}" max="{{ .max_value }}" placeholder="Enter a number ({{ .min_value }}-{{ .max_value }})" />
|
||||
{{ else }}
|
||||
<select class="form-select" name="value" id="value">
|
||||
{{ range $i, $option := .options }}
|
||||
<option value="{{ index $option "value" }}"{{ if eq $.current_value (index $option "value") }} selected{{ end }}>{{ index $option "label" }}</option>
|
||||
{{ end }}
|
||||
</select>
|
||||
{{ end }}
|
||||
<p class="form-input-hint">Default: <code>{{ .default_display }}</code></p>
|
||||
{{ end }}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button class="btn btn-primary" type="submit">{{ if lt $eklen 1 }}Create{{ else }}Save{{ end }}</button>
|
||||
<button class="btn btn-primary" type="submit" id="save-settings">{{ if eq .action "keybinding" }}Save{{ else if lt $eklen 1 }}Create{{ else }}Save{{ end }}</button>
|
||||
{{ if eq .action "environment" }}
|
||||
{{ if gt $eklen 0 }}
|
||||
<a class="btn" href="{{ $root }}settings">Cancel</a>
|
||||
@@ -67,6 +188,15 @@
|
||||
{{ else }}
|
||||
<a class="btn" href="{{ $root }}settings">Cancel</a>
|
||||
{{ end }}
|
||||
{{ else if eq .action "keybinding" }}
|
||||
<a class="btn" href="{{ $root }}settings">Cancel</a>
|
||||
<button class="btn float-right" type="button" onclick="document.getElementById('subaction').value='reset';this.form.submit()">Reset to Default</button>
|
||||
{{ else if eq .action "autocomplete" }}
|
||||
<a class="btn" href="{{ $root }}settings">Cancel</a>
|
||||
<button class="btn float-right" type="button" onclick="document.getElementById('subaction').value='reset';this.form.submit()">Reset to Default</button>
|
||||
{{ else if eq .action "search" }}
|
||||
<a class="btn" href="{{ $root }}settings">Cancel</a>
|
||||
<button class="btn float-right" type="button" onclick="document.getElementById('subaction').value='reset';this.form.submit()">Reset to Default</button>
|
||||
{{ else }}
|
||||
<button class="btn float-right" type="submit">Cancel</button>
|
||||
{{ end }}
|
||||
@@ -77,4 +207,4 @@
|
||||
</section>
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
@@ -7,8 +7,8 @@
|
||||
<link rel="stylesheet" href="{{ $root }}static/spectre.min.css">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<style type="text/css">
|
||||
.spacing-top { margin: 3px 0px 0px 0px; }
|
||||
.spacing-left { margin: 0px 3px 0px 0px; }
|
||||
.spacing-top { margin: 3px 0px 0px 0px !important; }
|
||||
.spacing-left { margin: 0px 3px 0px 0px !important; }
|
||||
.height-32px { height: 32px; }
|
||||
.align-center { text-align: center; }
|
||||
.pipe { color: #e85600; border-color: #e85600; }
|
||||
@@ -18,15 +18,26 @@
|
||||
.spacer-12px { width: 12px; display: inline-block; }
|
||||
.col-btn { width: 3.333%; }
|
||||
</style>
|
||||
<script type="text/javascript">
|
||||
function keycombo(elem, keys) {
|
||||
keys = keys.split(',');
|
||||
if (keys.length === 1 && keys[0].length < 1) {
|
||||
keys = [];
|
||||
}
|
||||
elem.textContent = keys.length > 0 ? keys.join(' + ') : "Not Assigned";
|
||||
}
|
||||
</script>
|
||||
<script>window.NOTEBOOK_KB={{ keybindings }};</script>
|
||||
<script src="{{ $root }}static/shortcuts.js"></script>
|
||||
<script type="text/javascript">
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
NotebookShortcuts.annotate(document.getElementById('nav-home'), 'cancel');
|
||||
NotebookShortcuts.annotate(document.getElementById('nav-about'), 'about');
|
||||
NotebookShortcuts.annotate(document.getElementById('nav-new-page'), 'new_page');
|
||||
NotebookShortcuts.bind([
|
||||
{ action: 'about', handler: function() { document.getElementById('nav-about').click(); } },
|
||||
{ action: 'new_page', handler: function() { document.getElementById('nav-new-page').click(); } },
|
||||
{ action: 'cancel', handler: function() { document.getElementById('nav-home').click(); } }
|
||||
]);
|
||||
|
||||
var labels = document.querySelectorAll('[data-shortcut-label]');
|
||||
for (var index = 0; index < labels.length; index++) {
|
||||
var element = labels[index];
|
||||
element.textContent = NotebookShortcuts.formatCombo(element.getAttribute('data-shortcut-label'));
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<section class="container grid-960 mt-10">
|
||||
@@ -36,7 +47,11 @@
|
||||
<a href="{{ $root }}" class="navbar-brand mr-10">Rizin Notebook</a>
|
||||
</section>
|
||||
<section class="navbar-section">
|
||||
<a href="{{ $root }}" class="btn btn-primary">Go Back</a>
|
||||
<a href="{{ $root }}about" class="btn" id="nav-about">About</a>
|
||||
|
||||
<a href="{{ $root }}new" class="btn" id="nav-new-page">New Page</a>
|
||||
|
||||
<a href="{{ $root }}" class="btn btn-primary" id="nav-home">Go Back</a>
|
||||
</section>
|
||||
</header>
|
||||
<section class="container">
|
||||
@@ -70,7 +85,94 @@
|
||||
New Environment Variable
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
<h3 class="s-title">Keyboard Shortcuts</h3>
|
||||
<div class="columns col-gapless">
|
||||
<div class="column col-1 col-btn spacing-left">
|
||||
{{ range $i, $kb := .kb_actions }}
|
||||
<a class="btn spacing-top" href="{{ $root }}settings/keybinding/edit/{{ index $kb "name" }}">
|
||||
<i class="icon icon-edit">Edit</i>
|
||||
</a>
|
||||
{{ end }}
|
||||
</div>
|
||||
<div class="column col-3 spacing-left">
|
||||
{{ range $i, $kb := .kb_actions }}
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon detail-size spacing-top height-32px">{{ index $kb "label" }}</span>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
<div class="column col-8">
|
||||
{{ range $i, $kb := .kb_actions }}
|
||||
<div class="input-group spacing-left">
|
||||
<span class="input-group-addon height-32px spacing-top" data-shortcut-label="{{ index $kb "combo" }}"></span>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
<a class="btn" href="{{ $root }}settings/keybinding/reset">
|
||||
Reset All to Defaults
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
<h3 class="s-title">Autocomplete Settings</h3>
|
||||
<div class="columns col-gapless">
|
||||
<div class="column col-1 col-btn spacing-left">
|
||||
{{ range $i, $ac := .ac_actions }}
|
||||
<a class="btn spacing-top" href="{{ $root }}settings/autocomplete/edit/{{ index $ac "name" }}">
|
||||
<i class="icon icon-edit">Edit</i>
|
||||
</a>
|
||||
{{ end }}
|
||||
</div>
|
||||
<div class="column col-3 spacing-left">
|
||||
{{ range $i, $ac := .ac_actions }}
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon detail-size spacing-top height-32px">{{ index $ac "label" }}</span>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
<div class="column col-8">
|
||||
{{ range $i, $ac := .ac_actions }}
|
||||
<div class="input-group spacing-left">
|
||||
<span class="input-group-addon height-32px spacing-top">{{ index $ac "value" }}</span>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
<a class="btn" href="{{ $root }}settings/autocomplete/reset">
|
||||
Reset to Defaults
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
<h3 class="s-title">Notebook Search Settings</h3>
|
||||
<div class="columns col-gapless">
|
||||
<div class="column col-1 col-btn spacing-left">
|
||||
{{ range $i, $ns := .ns_actions }}
|
||||
<a class="btn spacing-top" href="{{ $root }}settings/search/edit/{{ index $ns "name" }}">
|
||||
<i class="icon icon-edit">Edit</i>
|
||||
</a>
|
||||
{{ end }}
|
||||
</div>
|
||||
<div class="column col-3 spacing-left">
|
||||
{{ range $i, $ns := .ns_actions }}
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon detail-size spacing-top height-32px">{{ index $ns "label" }}</span>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
<div class="column col-8">
|
||||
{{ range $i, $ns := .ns_actions }}
|
||||
<div class="input-group spacing-left">
|
||||
<span class="input-group-addon height-32px spacing-top">{{ index $ns "value_display" }}</span>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
<a class="btn" href="{{ $root }}settings/search/reset">
|
||||
Reset to Defaults
|
||||
</a>
|
||||
</p>
|
||||
</section>
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
@@ -0,0 +1,109 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"html/template"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Autocomplete setting defaults and bounds.
|
||||
const (
|
||||
autocompleteSettingsPrefix = "ac:"
|
||||
|
||||
defaultACMaxResults = 12
|
||||
defaultACMinChars = 2
|
||||
|
||||
minACMaxResults = 1
|
||||
maxACMaxResults = 100
|
||||
minACMinChars = 1
|
||||
maxACMinChars = 10
|
||||
)
|
||||
|
||||
// AutocompleteSettingKeys lists the configurable autocomplete settings.
|
||||
var AutocompleteSettingKeys = []string{"max_results", "min_chars"}
|
||||
|
||||
// AutocompleteLabels maps setting keys to human-readable labels.
|
||||
var AutocompleteLabels = map[string]string{
|
||||
"max_results": "Max Results",
|
||||
"min_chars": "Minimum Characters",
|
||||
}
|
||||
|
||||
// AutocompleteDefaults maps setting keys to their default string values.
|
||||
var AutocompleteDefaults = map[string]string{
|
||||
"max_results": strconv.Itoa(defaultACMaxResults),
|
||||
"min_chars": strconv.Itoa(defaultACMinChars),
|
||||
}
|
||||
|
||||
// GetAutocompleteConfig returns the current autocomplete configuration,
|
||||
// merging database overrides with defaults and clamping values to safe bounds.
|
||||
func GetAutocompleteConfig() map[string]int {
|
||||
result := map[string]int{
|
||||
"max_results": defaultACMaxResults,
|
||||
"min_chars": defaultACMinChars,
|
||||
}
|
||||
|
||||
if catalog == nil {
|
||||
return result
|
||||
}
|
||||
|
||||
settings, _ := catalog.GetAllSettings()
|
||||
if settings == nil {
|
||||
return result
|
||||
}
|
||||
|
||||
for k, v := range settings {
|
||||
if !strings.HasPrefix(k, autocompleteSettingsPrefix) {
|
||||
continue
|
||||
}
|
||||
key := strings.TrimPrefix(k, autocompleteSettingsPrefix)
|
||||
val, err := strconv.Atoi(v)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
switch key {
|
||||
case "max_results":
|
||||
result["max_results"] = clampInt(val, minACMaxResults, maxACMaxResults)
|
||||
case "min_chars":
|
||||
result["min_chars"] = clampInt(val, minACMinChars, maxACMinChars)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// getAutocompleteConfigJSON returns the autocomplete config as a JSON
|
||||
// object suitable for embedding in a <script> tag.
|
||||
func getAutocompleteConfigJSON() template.JS {
|
||||
cfg := GetAutocompleteConfig()
|
||||
bytes, err := json.Marshal(cfg)
|
||||
if err != nil {
|
||||
return template.JS("{}")
|
||||
}
|
||||
return template.JS(bytes)
|
||||
}
|
||||
|
||||
// autocompleteActions returns ordered setting info for the settings page.
|
||||
func autocompleteActions() []map[string]string {
|
||||
cfg := GetAutocompleteConfig()
|
||||
actions := make([]map[string]string, 0, len(AutocompleteSettingKeys))
|
||||
for _, key := range AutocompleteSettingKeys {
|
||||
actions = append(actions, map[string]string{
|
||||
"name": key,
|
||||
"label": AutocompleteLabels[key],
|
||||
"value": strconv.Itoa(cfg[key]),
|
||||
"default_value": AutocompleteDefaults[key],
|
||||
})
|
||||
}
|
||||
return actions
|
||||
}
|
||||
|
||||
func clampInt(val, lo, hi int) int {
|
||||
if val < lo {
|
||||
return lo
|
||||
}
|
||||
if val > hi {
|
||||
return hi
|
||||
}
|
||||
return val
|
||||
}
|
||||
@@ -3,18 +3,13 @@ package main
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const (
|
||||
CONFIG_FILE = "config.json"
|
||||
KB_INDEX = "Index"
|
||||
KB_PAGE = "Page"
|
||||
)
|
||||
// ConfigFile is the filename for the notebook configuration JSON file.
|
||||
const ConfigFile = "config.json"
|
||||
|
||||
type NotebookConfig struct {
|
||||
Environment map[string]string `json:"environment"`
|
||||
@@ -22,31 +17,27 @@ type NotebookConfig struct {
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
func getValue(kmap map[string]string, action, defkey string) string {
|
||||
if kmap == nil {
|
||||
return defkey
|
||||
}
|
||||
if value, ok := kmap[action]; ok && len(value) > 0 {
|
||||
return value
|
||||
}
|
||||
return defkey
|
||||
}
|
||||
|
||||
func NewNotebookConfig(folder string) *NotebookConfig {
|
||||
var config = &NotebookConfig{}
|
||||
config.filename = path.Join(folder, CONFIG_FILE)
|
||||
bytes, err := ioutil.ReadFile(config.filename)
|
||||
nc := &NotebookConfig{}
|
||||
nc.filename = path.Join(folder, ConfigFile)
|
||||
|
||||
bytes, err := os.ReadFile(nc.filename)
|
||||
if err == nil {
|
||||
json.Unmarshal(bytes, config)
|
||||
}
|
||||
if config.Environment == nil {
|
||||
config.Environment = map[string]string{}
|
||||
if jsonErr := json.Unmarshal(bytes, nc); jsonErr != nil {
|
||||
fmt.Printf("warning: failed to parse config file: %v\n", jsonErr)
|
||||
}
|
||||
}
|
||||
|
||||
if value, ok := config.Environment["RIZIN_PATH"]; !ok || len(value) < 1 {
|
||||
config.Environment["RIZIN_PATH"] = os.Getenv("RIZIN_PATH")
|
||||
if nc.Environment == nil {
|
||||
nc.Environment = map[string]string{}
|
||||
}
|
||||
return config
|
||||
|
||||
// Auto-populate RIZIN_PATH from the OS environment if not configured.
|
||||
if value, ok := nc.Environment["RIZIN_PATH"]; !ok || len(value) < 1 {
|
||||
nc.Environment["RIZIN_PATH"] = os.Getenv("RIZIN_PATH")
|
||||
}
|
||||
|
||||
return nc
|
||||
}
|
||||
|
||||
func (nc *NotebookConfig) UpdateEnvironment() {
|
||||
@@ -56,29 +47,3 @@ func (nc *NotebookConfig) UpdateEnvironment() {
|
||||
os.Setenv(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
func (nc *NotebookConfig) DelEnvironment(key string) {
|
||||
nc.mutex.Lock()
|
||||
defer nc.mutex.Unlock()
|
||||
key = strings.TrimSpace(key)
|
||||
delete(nc.Environment, key)
|
||||
os.Unsetenv(key)
|
||||
}
|
||||
|
||||
func (nc *NotebookConfig) SetEnvironment(key, value string) {
|
||||
nc.mutex.Lock()
|
||||
defer nc.mutex.Unlock()
|
||||
value = strings.TrimSpace(value)
|
||||
key = strings.TrimSpace(key)
|
||||
os.Setenv(key, value)
|
||||
nc.Environment[key] = value
|
||||
}
|
||||
|
||||
func (nc *NotebookConfig) Save() {
|
||||
nc.mutex.Lock()
|
||||
defer nc.mutex.Unlock()
|
||||
bytes, _ := json.MarshalIndent(nc, "", "\t")
|
||||
if err := ioutil.WriteFile(nc.filename, bytes, 0644); err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(CutterNotebookPlugin LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_AUTOMOC ON)
|
||||
|
||||
# ── Locate the Cutter installation ──────────────────────────────────────────
|
||||
# Usage:
|
||||
# cmake -S . -B build -DCutter_DIR="C:/path/to/Cutter-v2.4.1-Windows-x86_64"
|
||||
# Also pass -DCMAKE_PREFIX_PATH="C:/Qt/6.7.2/msvc2019_64" for Qt6 SDK.
|
||||
|
||||
if(NOT Cutter_DIR AND NOT DEFINED ENV{Cutter_DIR})
|
||||
message(FATAL_ERROR
|
||||
"Set Cutter_DIR to your Cutter installation root, e.g.:\n"
|
||||
" cmake -S . -B build "
|
||||
"-DCutter_DIR=\"C:/path/to/Cutter-v2.4.1-Windows-x86_64\" "
|
||||
"-DCMAKE_PREFIX_PATH=\"C:/Qt/6.7.2/msvc2019_64\"\n"
|
||||
)
|
||||
endif()
|
||||
|
||||
if(NOT Cutter_DIR)
|
||||
set(Cutter_DIR "$ENV{Cutter_DIR}")
|
||||
endif()
|
||||
|
||||
file(TO_CMAKE_PATH "${Cutter_DIR}" CUTTER_ROOT)
|
||||
|
||||
# Normalise: if the user passed the cmake package dir, walk up.
|
||||
if(EXISTS "${CUTTER_ROOT}/CutterTargets.cmake")
|
||||
get_filename_component(CUTTER_ROOT "${CUTTER_ROOT}/../../.." ABSOLUTE)
|
||||
endif()
|
||||
|
||||
# Validate.
|
||||
if(NOT EXISTS "${CUTTER_ROOT}/include/cutter/plugins/CutterPlugin.h")
|
||||
message(FATAL_ERROR
|
||||
"Cannot find CutterPlugin.h in '${CUTTER_ROOT}/include/cutter/plugins'.\n"
|
||||
"Set Cutter_DIR to the root of an extracted Cutter binary release.\n"
|
||||
)
|
||||
endif()
|
||||
|
||||
message(STATUS "Cutter SDK root: ${CUTTER_ROOT}")
|
||||
|
||||
set(CUTTER_LIB_DIR "${CUTTER_ROOT}/lib")
|
||||
set(CUTTER_INCLUDE_DIR "${CUTTER_ROOT}/include")
|
||||
|
||||
# ── Find Qt6 from the system / CMAKE_PREFIX_PATH ───────────────────────────
|
||||
# We need Qt6 headers, MOC, and import libraries from a proper SDK install.
|
||||
# The Cutter binary release only ships runtime DLLs, not development files.
|
||||
find_package(Qt6 6.6 REQUIRED COMPONENTS Core Widgets Gui Network)
|
||||
|
||||
# ── Manually create Cutter::Cutter imported target ──────────────────────────
|
||||
# We bypass CutterConfig.cmake because it calls find_dependency(Qt6 …) for
|
||||
# components (Svg, Core5Compat, etc.) that we don't need, causing failures.
|
||||
add_library(Cutter::Cutter SHARED IMPORTED)
|
||||
if(WIN32)
|
||||
set_target_properties(Cutter::Cutter PROPERTIES
|
||||
IMPORTED_IMPLIB "${CUTTER_LIB_DIR}/cutter.lib"
|
||||
)
|
||||
elseif(APPLE)
|
||||
find_library(_cutter_lib NAMES cutter PATHS "${CUTTER_LIB_DIR}" NO_DEFAULT_PATH)
|
||||
set_target_properties(Cutter::Cutter PROPERTIES
|
||||
IMPORTED_LOCATION "${_cutter_lib}"
|
||||
)
|
||||
else()
|
||||
find_library(_cutter_lib NAMES cutter PATHS "${CUTTER_LIB_DIR}" NO_DEFAULT_PATH)
|
||||
set_target_properties(Cutter::Cutter PROPERTIES
|
||||
IMPORTED_LOCATION "${_cutter_lib}"
|
||||
)
|
||||
endif()
|
||||
|
||||
set_target_properties(Cutter::Cutter PROPERTIES
|
||||
INTERFACE_INCLUDE_DIRECTORIES
|
||||
"${CUTTER_INCLUDE_DIR}/cutter;${CUTTER_INCLUDE_DIR}/cutter/core;${CUTTER_INCLUDE_DIR}/cutter/widgets;${CUTTER_INCLUDE_DIR}/cutter/common;${CUTTER_INCLUDE_DIR}/cutter/plugins;${CUTTER_INCLUDE_DIR}/cutter/menus;${CUTTER_INCLUDE_DIR}/cutter/dialogs;${CUTTER_INCLUDE_DIR}/cutter/tools"
|
||||
INTERFACE_LINK_LIBRARIES
|
||||
"Qt6::Core;Qt6::Widgets;Qt6::Gui;Qt6::Network"
|
||||
)
|
||||
|
||||
# ── Rizin (include paths for rz_core.h etc.) ───────────────────────────────
|
||||
add_library(Cutter::Rizin INTERFACE IMPORTED)
|
||||
set_target_properties(Cutter::Rizin PROPERTIES
|
||||
INTERFACE_INCLUDE_DIRECTORIES "${CUTTER_INCLUDE_DIR}/librz;${CUTTER_INCLUDE_DIR}/librz/sdb"
|
||||
)
|
||||
|
||||
# ── Plugin install directory ────────────────────────────────────────────────
|
||||
if(WIN32)
|
||||
file(TO_CMAKE_PATH "$ENV{APPDATA}" _appdata)
|
||||
set(CUTTER_INSTALL_PLUGDIR "${_appdata}/rizin/cutter/plugins/native"
|
||||
CACHE STRING "Cutter native plugin install directory")
|
||||
elseif(APPLE)
|
||||
set(CUTTER_INSTALL_PLUGDIR "$ENV{HOME}/Library/Application Support/rizin/cutter/plugins/native"
|
||||
CACHE STRING "Cutter native plugin install directory")
|
||||
else()
|
||||
set(CUTTER_INSTALL_PLUGDIR "$ENV{HOME}/.local/share/rizin/cutter/plugins/native"
|
||||
CACHE STRING "Cutter native plugin install directory")
|
||||
endif()
|
||||
|
||||
# ── Plugin shared library ──────────────────────────────────────────────────
|
||||
add_library(CutterNotebookPlugin MODULE
|
||||
src/CutterNotebookPlugin.cpp
|
||||
src/CutterNotebookPlugin.h
|
||||
)
|
||||
|
||||
target_link_libraries(CutterNotebookPlugin PRIVATE
|
||||
Cutter::Cutter
|
||||
Cutter::Rizin
|
||||
Qt6::Core
|
||||
Qt6::Network
|
||||
Qt6::Widgets
|
||||
)
|
||||
|
||||
install(TARGETS CutterNotebookPlugin DESTINATION "${CUTTER_INSTALL_PLUGDIR}")
|
||||
@@ -0,0 +1,115 @@
|
||||
# cutter-notebook-plugin
|
||||
|
||||
Adds a Plugins -> Notebook dock and menu to Cutter for interacting with the
|
||||
[rizin-notebook](../README.md) server.
|
||||
|
||||
This plugin talks to the notebook server directly over HTTP using Qt's network stack.
|
||||
It does not depend on the `NB` commands from the rizin core plugin for its UI functionality,
|
||||
but the core plugin must be installed for any `NB` command functionality in Cutter's
|
||||
rizin shell.
|
||||
|
||||
## What you get
|
||||
|
||||
A dock panel with two tabs:
|
||||
|
||||
**Pages tab:**
|
||||
- List of all notebook pages
|
||||
- Per-page cell viewer showing cell type, content, and output
|
||||
- Buttons: Refresh Pages, New Page, Delete Page, Attach Binary, Open Pipe, Close Pipe,
|
||||
Reload Page, Export, Import
|
||||
|
||||
**Compose tab:**
|
||||
- Active page and pipe status
|
||||
- Editor for writing and submitting cells:
|
||||
- Markdown note
|
||||
- Command cell (recorded without execution)
|
||||
- Script cell (recorded without execution)
|
||||
- Run command now (executes via server pipe, records result)
|
||||
- Run script now (executes JavaScript on server, records result)
|
||||
|
||||
**Menu entries** (Plugins -> Notebook):
|
||||
- Connect... - connect to a running server
|
||||
- Spawn Server - auto-start `rizin-notebook.exe`
|
||||
- Server Status - show server version, rizin path, storage, page count
|
||||
- List Pages - refresh the pages tab
|
||||
- New Page... - create a page (auto-attaches the currently open binary)
|
||||
- Delete Page - delete the selected page
|
||||
- Attach Binary... - attach a binary file to the selected page
|
||||
- Export Page... - save the selected page as a `.rznb` file
|
||||
- Import Page... - import a `.rznb` file as a new page
|
||||
- (separator)
|
||||
- Open in Browser - open the server web UI in your browser
|
||||
- Set Server URL... - change the server address
|
||||
|
||||
**Status bar** (bottom of dock): shows connection state, page count, open pipe count,
|
||||
and server details on hover.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Cutter 2.4.1 (or compatible)
|
||||
- `rz_notebook` core plugin installed and loaded in Cutter (provides `NB` commands for
|
||||
the rizin shell; the Cutter plugin itself uses the HTTP JSON API directly)
|
||||
- `rizin-notebook` Go server running (the Cutter plugin can auto-start it)
|
||||
- Qt 6.7.x SDK with MSVC 2019 64-bit (Windows) or the matching platform toolkit
|
||||
- CMake 3.16+ and a C++20 compiler
|
||||
|
||||
## Build (Windows)
|
||||
|
||||
Install Qt 6.7.2 if you don't have it:
|
||||
|
||||
```powershell
|
||||
pip install aqtinstall
|
||||
python -m aqt install-qt windows desktop 6.7.2 win64_msvc2019_64 --outputdir C:\Qt
|
||||
```
|
||||
|
||||
Configure and build:
|
||||
|
||||
```powershell
|
||||
cmake -S cutter-notebook-plugin -B cutter-notebook-plugin/build ^
|
||||
-DCutter_DIR="C:\path\to\Cutter-v2.4.1-Windows-x86_64" ^
|
||||
-DQt6_DIR="C:\Qt\6.7.2\msvc2019_64\lib\cmake\Qt6"
|
||||
|
||||
cmake --build cutter-notebook-plugin/build --config Release
|
||||
```
|
||||
|
||||
## Build (Linux / macOS)
|
||||
|
||||
```bash
|
||||
cmake -S cutter-notebook-plugin -B cutter-notebook-plugin/build \
|
||||
-DCutter_DIR=/opt/Cutter \
|
||||
-DCMAKE_PREFIX_PATH=/path/to/Qt/6.x/gcc_64
|
||||
|
||||
cmake --build cutter-notebook-plugin/build --config Release
|
||||
```
|
||||
|
||||
## Install
|
||||
|
||||
Copy `CutterNotebookPlugin.dll` (Windows) or `CutterNotebookPlugin.so` (Linux) to:
|
||||
|
||||
- Windows: `%APPDATA%\rizin\cutter\plugins\native`
|
||||
- Linux: `~/.local/share/rizin/cutter/plugins/native`
|
||||
- macOS: `~/Library/Application Support/rizin/cutter/plugins/native`
|
||||
|
||||
The `rz_notebook` core plugin (`rz_notebook.dll` / `.so`) goes into the Rizin plugin
|
||||
directory that Cutter uses. For portable Cutter builds, this is often inside the Cutter
|
||||
installation directory, not just the user plugin directory.
|
||||
|
||||
Run `rizin -H RZ_USER_PLUGINS` to find the user path. For portable Cutter, also check
|
||||
`<cutter-root>/lib/rizin/plugins/`.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Dock does not appear** - make sure `CutterNotebookPlugin.dll` is in the native plugins
|
||||
directory and was built against the same Cutter and Qt version you are running.
|
||||
|
||||
**Server not reachable** - start `rizin-notebook` first, or use Spawn Server from the menu.
|
||||
Use Set Server URL if the server runs on a non-default address.
|
||||
|
||||
**NB commands not available in Cutter's rizin shell** - `rz_notebook.dll` (the core plugin)
|
||||
must be built against the same Rizin that Cutter bundles and placed in the Rizin plugin
|
||||
path that Cutter loads from. The Cutter plugin itself does not require this for its dock UI,
|
||||
but you need it to use `NB` commands in Cutter's command panel.
|
||||
|
||||
**Binary hash mismatch warning** - when you open a page whose attached binary differs from
|
||||
the file currently open in Cutter, the plugin shows a warning. You can re-attach the
|
||||
correct binary via Attach Binary.
|
||||
@@ -0,0 +1,107 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <CutterPlugin.h>
|
||||
|
||||
class QMenu;
|
||||
class QDockWidget;
|
||||
class QComboBox;
|
||||
class QLabel;
|
||||
class QListWidget;
|
||||
class QPlainTextEdit;
|
||||
class QTabWidget;
|
||||
class MainWindow;
|
||||
|
||||
class CutterNotebookPlugin final : public QObject, public CutterPlugin {
|
||||
Q_OBJECT
|
||||
Q_PLUGIN_METADATA(IID "re.rizin.cutter.plugins.CutterPlugin")
|
||||
Q_INTERFACES(CutterPlugin)
|
||||
|
||||
public:
|
||||
void setupPlugin() override;
|
||||
void setupInterface(MainWindow *main) override;
|
||||
void terminate() override;
|
||||
|
||||
QString getName() const override { return "Notebook"; }
|
||||
QString getAuthor() const override { return "rizin-notebook"; }
|
||||
QString getDescription() const override {
|
||||
return "Provides a native Cutter client for the shared rizin-notebook server.";
|
||||
}
|
||||
QString getVersion() const override { return "2.0.0"; }
|
||||
|
||||
private slots:
|
||||
void onConnect();
|
||||
void onSpawn();
|
||||
void onServerStatus();
|
||||
void onListPages();
|
||||
void onNewPage();
|
||||
void onDeletePage();
|
||||
void onAttachBinary();
|
||||
void onOpenPipe();
|
||||
void onClosePipe();
|
||||
void onSubmitEditor();
|
||||
void onReloadSelectedPage();
|
||||
void onOpenBrowser();
|
||||
void onSetUrl();
|
||||
void onExportPage();
|
||||
void onImportPage();
|
||||
|
||||
private:
|
||||
QString runCmd(const QString &cmd, bool *ok = nullptr);
|
||||
QString notebookBaseUrl(bool *fromNBPlugin = nullptr);
|
||||
void setNotebookBaseUrl(const QString &url);
|
||||
bool ensureNotebookReady(bool showDialog = true);
|
||||
QByteArray sendJsonRequest(const QString &method,
|
||||
const QString &path,
|
||||
const QByteArray &body = QByteArray(),
|
||||
int *statusCode = nullptr,
|
||||
QString *errorOut = nullptr);
|
||||
QByteArray sendRawGetRequest(const QString &path,
|
||||
int *statusCode = nullptr,
|
||||
QString *contentDisposition = nullptr,
|
||||
QString *errorOut = nullptr);
|
||||
QByteArray sendMultipartPost(const QString &path,
|
||||
const QString &fieldName,
|
||||
const QString &fileName,
|
||||
const QByteArray &fileData,
|
||||
int *statusCode = nullptr,
|
||||
QString *errorOut = nullptr);
|
||||
bool populatePages(bool keepSelection = true);
|
||||
bool loadPage(const QString &pageId, bool focusView = false);
|
||||
bool attachBinaryToPage(const QString &pageId, const QString &filePath = QString());
|
||||
void renderPage(const QVariantMap &page);
|
||||
QString selectedPageId() const;
|
||||
void setActivePage(const QString &pageId);
|
||||
void updateComposeLabels(const QVariantMap &page = QVariantMap());
|
||||
void updateEditorPlaceholder();
|
||||
void refreshDockStatus();
|
||||
void ensureDockVisible();
|
||||
void updateConnectionUI();
|
||||
QString computeFileHash(const QString &filePath);
|
||||
void verifyBinaryHash(const QString &serverHash);
|
||||
QString currentBinaryPath();
|
||||
void fetchCommandsForCompleter();
|
||||
void fetchAutocompleteSettings();
|
||||
void updateAutocomplete();
|
||||
void acceptAutocomplete();
|
||||
bool eventFilter(QObject *obj, QEvent *event) override;
|
||||
|
||||
MainWindow *mainWindow = nullptr;
|
||||
QDockWidget *dockWidget = nullptr;
|
||||
QLabel *statusLabel = nullptr;
|
||||
QLabel *currentPageLabel = nullptr;
|
||||
QLabel *pipeLabel = nullptr;
|
||||
QListWidget *pageListWidget = nullptr;
|
||||
QPlainTextEdit *pageView = nullptr;
|
||||
QPlainTextEdit *editor = nullptr;
|
||||
QComboBox *editorMode = nullptr;
|
||||
QTabWidget *dockTabs = nullptr;
|
||||
QListWidget *acPopup = nullptr;
|
||||
QStringList cmdCommandList;
|
||||
int maxCompleterResults = 12;
|
||||
int minCompleterChars = 2;
|
||||
QString currentServerUrl;
|
||||
QString activePageId;
|
||||
bool connected = false;
|
||||
bool m_populatingPages = false;
|
||||
};
|
||||
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 143 KiB |
|
After Width: | Height: | Size: 42 KiB |
@@ -0,0 +1,57 @@
|
||||
module github.com/rizinorg/rizin-notebook
|
||||
|
||||
go 1.24.0
|
||||
|
||||
toolchain go1.24.1
|
||||
|
||||
require (
|
||||
github.com/dop251/goja v0.0.0-20251008123653-cf18d89f3cf6
|
||||
github.com/gin-gonic/gin v1.11.0
|
||||
golang.org/x/sync v0.17.0
|
||||
google.golang.org/protobuf v1.36.11
|
||||
modernc.org/sqlite v1.46.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||
github.com/bytedance/sonic v1.14.2 // indirect
|
||||
github.com/bytedance/sonic/loader v0.4.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.11 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.28.0 // indirect
|
||||
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/goccy/go-yaml v1.18.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20251007162407-5df77e3f7d1d // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/quic-go/qpack v0.5.1 // indirect
|
||||
github.com/quic-go/quic-go v0.55.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||
go.uber.org/mock v0.6.0 // indirect
|
||||
golang.org/x/arch v0.22.0 // indirect
|
||||
golang.org/x/crypto v0.43.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
|
||||
golang.org/x/mod v0.29.0 // indirect
|
||||
golang.org/x/net v0.46.0 // indirect
|
||||
golang.org/x/sys v0.37.0 // indirect
|
||||
golang.org/x/text v0.30.0 // indirect
|
||||
golang.org/x/tools v0.38.0 // indirect
|
||||
modernc.org/libc v1.67.6 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
)
|
||||
@@ -0,0 +1,145 @@
|
||||
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
|
||||
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
||||
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
||||
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
||||
github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE=
|
||||
github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980=
|
||||
github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o=
|
||||
github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
||||
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dop251/goja v0.0.0-20251008123653-cf18d89f3cf6 h1:6dE1TmjqkY6tehR4A67gDNhvDtuZ54ocu7ab4K9o540=
|
||||
github.com/dop251/goja v0.0.0-20251008123653-cf18d89f3cf6/go.mod h1:MxLav0peU43GgvwVgNbLAj1s/bSGboKkhuULvq/7hx4=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik=
|
||||
github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
|
||||
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
|
||||
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
|
||||
github.com/go-sourcemap/sourcemap v2.1.4+incompatible h1:a+iTbH5auLKxaNwQFg0B+TCYl6lbukKPc7b5x0n1s6Q=
|
||||
github.com/go-sourcemap/sourcemap v2.1.4+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20251007162407-5df77e3f7d1d h1:KJIErDwbSHjnp/SGzE5ed8Aol7JsKiI5X7yWKAtzhM0=
|
||||
github.com/google/pprof v0.0.0-20251007162407-5df77e3f7d1d/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||
github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk=
|
||||
github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
||||
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
|
||||
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
||||
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
|
||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
|
||||
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
||||
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
||||
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
||||
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
|
||||
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc=
|
||||
modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM=
|
||||
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
|
||||
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE=
|
||||
modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||
modernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI=
|
||||
modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU=
|
||||
modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
@@ -0,0 +1,91 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"html/template"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var DefaultKeybindings = map[string]string{
|
||||
"about": "alt+a",
|
||||
"new_page": "ctrl+n",
|
||||
"settings": "alt+s",
|
||||
"toggle_pipe": "alt+o",
|
||||
"new_command": "alt+c",
|
||||
"new_markdown": "alt+m",
|
||||
"new_script": "alt+j",
|
||||
"search_notebook": "mod+shift+f",
|
||||
"save": "mod+s",
|
||||
"cancel": "escape",
|
||||
"execute": "ctrl+enter",
|
||||
"edit_markdown": "alt+e",
|
||||
}
|
||||
|
||||
var KeybindingOrder = []string{
|
||||
"about", "new_page", "settings", "toggle_pipe",
|
||||
"new_command", "new_markdown", "new_script", "search_notebook",
|
||||
"save", "execute", "cancel", "edit_markdown",
|
||||
}
|
||||
|
||||
var KeybindingLabels = map[string]string{
|
||||
"about": "About",
|
||||
"new_page": "New Page",
|
||||
"settings": "Settings",
|
||||
"toggle_pipe": "Open / Close Pipe",
|
||||
"new_command": "Command Line",
|
||||
"new_markdown": "New Markdown",
|
||||
"new_script": "New Script",
|
||||
"search_notebook": "Notebook Search",
|
||||
"save": "Save",
|
||||
"cancel": "Cancel / Go Back",
|
||||
"execute": "Execute / Run",
|
||||
"edit_markdown": "Edit Markdown",
|
||||
}
|
||||
|
||||
const keybindingSettingsPrefix = "kb:"
|
||||
|
||||
func GetAllKeybindings() map[string]string {
|
||||
result := make(map[string]string, len(DefaultKeybindings))
|
||||
for k, v := range DefaultKeybindings {
|
||||
result[k] = v
|
||||
}
|
||||
if catalog == nil {
|
||||
return result
|
||||
}
|
||||
settings, _ := catalog.GetAllSettings()
|
||||
if settings == nil {
|
||||
return result
|
||||
}
|
||||
for k, v := range settings {
|
||||
if strings.HasPrefix(k, keybindingSettingsPrefix) {
|
||||
action := strings.TrimPrefix(k, keybindingSettingsPrefix)
|
||||
if _, ok := DefaultKeybindings[action]; ok && len(v) > 0 {
|
||||
result[action] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func getKeybindingsJSON() template.JS {
|
||||
kb := GetAllKeybindings()
|
||||
bytes, err := json.Marshal(kb)
|
||||
if err != nil {
|
||||
return template.JS("{}")
|
||||
}
|
||||
return template.JS(bytes)
|
||||
}
|
||||
|
||||
func keybindingActions() []map[string]string {
|
||||
kb := GetAllKeybindings()
|
||||
actions := make([]map[string]string, 0, len(KeybindingOrder))
|
||||
for _, name := range KeybindingOrder {
|
||||
actions = append(actions, map[string]string{
|
||||
"name": name,
|
||||
"label": KeybindingLabels[name],
|
||||
"combo": kb[name],
|
||||
"default_combo": DefaultKeybindings[name],
|
||||
})
|
||||
}
|
||||
return actions
|
||||
}
|
||||
@@ -1,61 +1,192 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path"
|
||||
"runtime/debug"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
NBVERSION string // Notebook version string
|
||||
NBVERSION string
|
||||
webroot string
|
||||
notebook *Notebook // Notebook object
|
||||
config *NotebookConfig // NotebookConfig object
|
||||
notebook *Notebook
|
||||
config *NotebookConfig
|
||||
catalog *Catalog
|
||||
)
|
||||
|
||||
func usage() {
|
||||
flag.PrintDefaults()
|
||||
fmt.Fprintf(os.Stderr, "Environment vars:\n RIZIN_PATH\n overrides where rizin executable is installed\n")
|
||||
fmt.Fprintf(os.Stderr, "Environment vars:\n RIZIN_PATH\n \toverrides where rizin executable is installed\n")
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
var debug bool
|
||||
var assets string
|
||||
var bind string
|
||||
var rizinbin = "rizin"
|
||||
var dataDir string = ".rizin-notebook"
|
||||
var dataDir = ".rizin-notebook"
|
||||
|
||||
if len(NBVERSION) < 1 {
|
||||
NBVERSION = "unknown"
|
||||
if len(NBVERSION) < 1 || NBVERSION == "unknown" {
|
||||
NBVERSION = resolveNotebookVersion()
|
||||
}
|
||||
|
||||
if homedir, err := os.UserHomeDir(); err == nil {
|
||||
dataDir = path.Join(homedir, ".rizin-notebook")
|
||||
}
|
||||
|
||||
config = NewNotebookConfig(dataDir)
|
||||
config.UpdateEnvironment()
|
||||
|
||||
if loc := os.Getenv("RIZIN_PATH"); len(loc) > 1 {
|
||||
rizinbin = loc
|
||||
}
|
||||
|
||||
// Parse flags BEFORE using dataDir to create config or notebook,
|
||||
// so that the -notebook flag is respected on first load.
|
||||
flag.StringVar(&bind, "bind", "127.0.0.1:8000", "[address]:[port] address to bind to.")
|
||||
flag.StringVar(&webroot, "root", "/", "defines where the web root of the application is.")
|
||||
flag.StringVar(&dataDir, "notebook", ""+dataDir, "defines where the notebook folder is located.")
|
||||
flag.StringVar(&dataDir, "notebook", dataDir, "defines where the notebook folder is located.")
|
||||
flag.StringVar(&assets, "debug-assets", "", "allows you to debug the assets (-debug-assets /path/to/assets).")
|
||||
flag.BoolVar(&debug, "debug", false, "enable http debug logs.")
|
||||
flag.Usage = usage
|
||||
flag.Parse()
|
||||
|
||||
// Ensure the data directory exists.
|
||||
if err := os.MkdirAll(dataDir, os.ModePerm); err != nil && !os.IsExist(err) {
|
||||
panic(err)
|
||||
fmt.Fprintf(os.Stderr, "fatal: cannot create data directory: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Load config AFTER flags are parsed so -notebook is respected.
|
||||
config = NewNotebookConfig(dataDir)
|
||||
config.UpdateEnvironment()
|
||||
|
||||
// Initialize catalog (per-page .rznb storage).
|
||||
var err error
|
||||
catalog, err = NewCatalog(dataDir)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "fatal: cannot open catalog: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer catalog.Close()
|
||||
|
||||
// Migrate legacy monolithic notebook.db into per-page .rznb files.
|
||||
if migrated, merr := catalog.MigrateFromLegacyDB(dataDir); merr != nil {
|
||||
fmt.Printf("warning: legacy DB migration error: %v\n", merr)
|
||||
} else if migrated > 0 {
|
||||
fmt.Printf("Migrated %d pages from legacy notebook.db to .rznb files.\n", migrated)
|
||||
}
|
||||
|
||||
// Import JSON page data into .rznb files if present.
|
||||
if migrated, merr := catalog.MigrateFromJSON(dataDir); merr != nil {
|
||||
fmt.Printf("warning: JSON migration error: %v\n", merr)
|
||||
} else if migrated > 0 {
|
||||
fmt.Printf("Migrated %d pages from JSON to .rznb files.\n", migrated)
|
||||
}
|
||||
if merr := catalog.MigrateSettings(dataDir); merr != nil {
|
||||
fmt.Printf("warning: settings migration error: %v\n", merr)
|
||||
}
|
||||
|
||||
// Allow RIZIN_PATH env var to override the default rizin binary location.
|
||||
if loc := os.Getenv("RIZIN_PATH"); len(loc) > 1 {
|
||||
rizinbin = loc
|
||||
}
|
||||
|
||||
// Prevent the rz_notebook plugin from auto-starting another server
|
||||
// instance when we spawn rizin sub-processes (to load commands, etc.).
|
||||
os.Setenv("RIZIN_NOTEBOOK_NO_AUTOSTART", "1")
|
||||
|
||||
fmt.Printf("Server data dir '%s'\n", dataDir)
|
||||
|
||||
// Create notebook WITHOUT loading commands yet; the HTTP server
|
||||
// must be listening BEFORE we spawn any rizin sub-process, so that
|
||||
// the rz_notebook plugin inside those sub-processes sees us as alive
|
||||
// and does not try to start yet another server instance.
|
||||
notebook = NewNotebook(dataDir, rizinbin)
|
||||
|
||||
runServer(assets, bind, debug)
|
||||
// Start the HTTP server FIRST (synchronous listen, async serve).
|
||||
srv := startServer(assets, bind, debug)
|
||||
|
||||
// NOW load rizin commands; any rizin sub-process will find us alive.
|
||||
notebook.LoadCommands()
|
||||
|
||||
// Block until SIGINT or SIGTERM is received.
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-quit
|
||||
|
||||
fmt.Println("\nShutting down server...")
|
||||
|
||||
// Close all open Rizin pipes to prevent orphan processes.
|
||||
notebook.closeAllPipes()
|
||||
|
||||
// Gracefully shut down the HTTP server with a 5-second timeout.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
if err := srv.Shutdown(ctx); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "server shutdown error: %v\n", err)
|
||||
}
|
||||
fmt.Println("Server stopped.")
|
||||
}
|
||||
|
||||
func resolveNotebookVersion() string {
|
||||
bi, ok := debug.ReadBuildInfo()
|
||||
if !ok {
|
||||
return "dev"
|
||||
}
|
||||
|
||||
if bi.Main.Version != "" && bi.Main.Version != "(devel)" {
|
||||
return bi.Main.Version
|
||||
}
|
||||
|
||||
var rev string
|
||||
var dirty bool
|
||||
for _, s := range bi.Settings {
|
||||
switch s.Key {
|
||||
case "vcs.revision":
|
||||
rev = s.Value
|
||||
case "vcs.modified":
|
||||
dirty = s.Value == "true"
|
||||
}
|
||||
}
|
||||
|
||||
if rev == "" {
|
||||
return "dev"
|
||||
}
|
||||
|
||||
if len(rev) > 12 {
|
||||
rev = rev[:12]
|
||||
}
|
||||
|
||||
if dirty {
|
||||
return "dev-" + rev + "-dirty"
|
||||
}
|
||||
return "dev-" + rev
|
||||
}
|
||||
|
||||
func startServer(assets, bind string, debug bool) *http.Server {
|
||||
router := setupRouter(assets, bind, debug)
|
||||
srv := &http.Server{
|
||||
Handler: router,
|
||||
}
|
||||
|
||||
// Bind the socket synchronously so the server is reachable
|
||||
// before we return. This guarantees that any rizin sub-process
|
||||
// spawned later will find the health-check endpoint alive.
|
||||
ln, err := net.Listen("tcp", bind)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "fatal: listen error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("Server listening at http://%s\n", bind)
|
||||
|
||||
go func() {
|
||||
if err := srv.Serve(ln); err != nil && err != http.ErrServerClosed {
|
||||
fmt.Fprintf(os.Stderr, "serve error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}()
|
||||
|
||||
return srv
|
||||
}
|
||||
@@ -2,25 +2,38 @@ package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const ELEMENT_NONCE_SIZE = 8
|
||||
const PAGE_NONCE_SIZE = 32
|
||||
// ElementNonceSize is the length of nonce strings used for page elements.
|
||||
const ElementNonceSize = 8
|
||||
|
||||
// PageNonceSize is the length of nonce strings used for notebook pages.
|
||||
const PageNonceSize = 32
|
||||
|
||||
// nonceAlphabet is the character set used for generating nonce strings.
|
||||
const nonceAlphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
||||
|
||||
func Nonce(n int) string {
|
||||
const encoding = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
||||
const modulo = byte(len(encoding))
|
||||
const modulo = byte(len(nonceAlphabet))
|
||||
bytes := make([]byte, n)
|
||||
_, err := rand.Read(bytes)
|
||||
if err != nil {
|
||||
if _, err := rand.Read(bytes); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
for i, b := range bytes {
|
||||
bytes[i] = encoding[b%modulo]
|
||||
bytes[i] = nonceAlphabet[b%modulo]
|
||||
}
|
||||
return string(bytes)
|
||||
}
|
||||
|
||||
func IsValidNonce(unique string) bool {
|
||||
func IsValidNonce(nonce string, expectedLen int) bool {
|
||||
if len(nonce) != expectedLen {
|
||||
return false
|
||||
}
|
||||
for _, c := range nonce {
|
||||
if !strings.ContainsRune(nonceAlphabet, c) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -1,348 +1,144 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/gin-gonic/gin"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const PAGE_FILE = "page.json"
|
||||
|
||||
func readJson(filepath string) gin.H {
|
||||
var data = gin.H{}
|
||||
bytes, err := ioutil.ReadFile(filepath)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
json.Unmarshal(bytes, &data)
|
||||
return data
|
||||
}
|
||||
|
||||
func sanitizeLines(lines []interface{}) []interface{} {
|
||||
for i, v := range lines {
|
||||
n := gin.H{}
|
||||
p := v.(map[string]interface{})
|
||||
for k, _ := range p {
|
||||
n[k] = p[k]
|
||||
}
|
||||
lines[i] = n
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
func findLineByKey(lines []interface{}, key, eunique string) int {
|
||||
for i, n := range lines {
|
||||
p := n.(gin.H)
|
||||
if v, ok := p[key]; ok && eunique == v {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func findNonce(a []string, b string) int {
|
||||
for i, n := range a {
|
||||
if n == b {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
type Notebook struct {
|
||||
mutex sync.Mutex
|
||||
pages gin.H
|
||||
uniques []string
|
||||
storage string
|
||||
pipes map[string]*Rizin
|
||||
jsvm *JavaScript
|
||||
rizin string
|
||||
cmds map[string]RizinCommand
|
||||
rizinInfoCache []string
|
||||
}
|
||||
|
||||
func NewNotebook(storage, rizinbin string) *Notebook {
|
||||
pages := gin.H{}
|
||||
uniques := []string{}
|
||||
prefix := path.Join(storage) + string(os.PathSeparator)
|
||||
suffix := string(os.PathSeparator) + PAGE_FILE
|
||||
jsvm := NewJavaScript()
|
||||
if jsvm == nil {
|
||||
panic("failed to create scripting engine")
|
||||
}
|
||||
|
||||
files, err := filepath.Glob(path.Join(storage, "*", PAGE_FILE))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
for _, file := range files {
|
||||
page := readJson(file)
|
||||
page["lines"] = sanitizeLines(page["lines"].([]interface{}))
|
||||
unique := strings.TrimSuffix(strings.TrimPrefix(file, prefix), suffix)
|
||||
page["unique"] = unique
|
||||
pages[unique] = page
|
||||
uniques = append(uniques, unique)
|
||||
}
|
||||
sort.Strings(uniques)
|
||||
return &Notebook{
|
||||
pages: pages,
|
||||
uniques: uniques,
|
||||
storage: storage,
|
||||
pipes: map[string]*Rizin{},
|
||||
jsvm: jsvm,
|
||||
rizin: rizinbin,
|
||||
cmds: map[string]RizinCommand{},
|
||||
rizinInfoCache: nil,
|
||||
}
|
||||
}
|
||||
|
||||
func (n *Notebook) list() []gin.H {
|
||||
n.mutex.Lock()
|
||||
defer n.mutex.Unlock()
|
||||
pages := make([]gin.H, len(n.uniques))
|
||||
for i, unique := range n.uniques {
|
||||
page := n.pages[unique].(gin.H)
|
||||
pipe := n.pipes[unique]
|
||||
|
||||
pages[i] = gin.H{
|
||||
"title": page["title"],
|
||||
"unique": unique,
|
||||
"pipe": pipe != nil,
|
||||
}
|
||||
// LoadCommands fetches the list of rizin commands by spawning rizin.
|
||||
// This MUST be called after the HTTP server is listening, because the
|
||||
// spawned rizin process loads rz_notebook which health-checks us.
|
||||
func (n *Notebook) LoadCommands() {
|
||||
cmds, err := RizinCommands(n.rizin)
|
||||
if err != nil {
|
||||
fmt.Printf("warning: failed to load rizin commands: %v\n", err)
|
||||
}
|
||||
|
||||
return pages
|
||||
info, infoErr := RizinInfo(n.rizin)
|
||||
if infoErr != nil {
|
||||
fmt.Printf("warning: failed to load rizin info: %v\n", infoErr)
|
||||
}
|
||||
|
||||
n.mutex.Lock()
|
||||
if cmds != nil {
|
||||
n.cmds = cmds
|
||||
}
|
||||
if len(info) > 0 {
|
||||
n.rizinInfoCache = append([]string(nil), info...)
|
||||
}
|
||||
n.mutex.Unlock()
|
||||
}
|
||||
|
||||
func (n *Notebook) info() ([]string, error) {
|
||||
return RizinInfo(n.rizin)
|
||||
}
|
||||
|
||||
func (n *Notebook) get(unique string) gin.H {
|
||||
if len(unique) != PAGE_NONCE_SIZE {
|
||||
return nil
|
||||
}
|
||||
n.mutex.Lock()
|
||||
defer n.mutex.Unlock()
|
||||
if page, ok := n.pages[unique]; ok {
|
||||
return page.(gin.H)
|
||||
if len(n.rizinInfoCache) > 0 {
|
||||
info := append([]string(nil), n.rizinInfoCache...)
|
||||
n.mutex.Unlock()
|
||||
return info, nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
n.mutex.Unlock()
|
||||
|
||||
func (n *Notebook) newmd(unique string) string {
|
||||
if len(unique) != PAGE_NONCE_SIZE {
|
||||
return ""
|
||||
}
|
||||
n.mutex.Lock()
|
||||
defer n.mutex.Unlock()
|
||||
if data, ok := n.pages[unique]; ok {
|
||||
page := data.(gin.H)
|
||||
var eunique = Nonce(ELEMENT_NONCE_SIZE)
|
||||
for {
|
||||
if _, err := n.file(unique, eunique+".md"); err != nil {
|
||||
break
|
||||
}
|
||||
eunique = Nonce(ELEMENT_NONCE_SIZE)
|
||||
}
|
||||
if !n.save([]byte{}, unique, eunique+".md") {
|
||||
return ""
|
||||
}
|
||||
page["lines"] = append(page["lines"].([]interface{}), gin.H{
|
||||
"type": "markdown",
|
||||
"unique": eunique,
|
||||
})
|
||||
filepath := path.Join(n.storage, unique, PAGE_FILE)
|
||||
bytes, _ := json.MarshalIndent(page, "", "\t")
|
||||
if ioutil.WriteFile(filepath, bytes, 0644) == nil {
|
||||
n.pages[unique] = page
|
||||
return eunique
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (n *Notebook) newcmd(unique, command string) string {
|
||||
if len(unique) != PAGE_NONCE_SIZE {
|
||||
return ""
|
||||
}
|
||||
n.mutex.Lock()
|
||||
defer n.mutex.Unlock()
|
||||
if data, ok := n.pages[unique]; ok {
|
||||
page := data.(gin.H)
|
||||
var eunique = Nonce(ELEMENT_NONCE_SIZE)
|
||||
for {
|
||||
if _, err := n.file(unique, eunique+".out"); err != nil {
|
||||
break
|
||||
}
|
||||
eunique = Nonce(ELEMENT_NONCE_SIZE)
|
||||
}
|
||||
page["lines"] = append(page["lines"].([]interface{}), gin.H{
|
||||
"type": "command",
|
||||
"unique": eunique,
|
||||
"command": command,
|
||||
})
|
||||
filepath := path.Join(n.storage, unique, PAGE_FILE)
|
||||
bytes, _ := json.MarshalIndent(page, "", "\t")
|
||||
if ioutil.WriteFile(filepath, bytes, 0644) == nil {
|
||||
n.pages[unique] = page
|
||||
return eunique
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (n *Notebook) deleteElem(unique, eunique string, markdown bool) bool {
|
||||
if len(unique) != PAGE_NONCE_SIZE || len(eunique) != ELEMENT_NONCE_SIZE {
|
||||
return false
|
||||
}
|
||||
n.mutex.Lock()
|
||||
defer n.mutex.Unlock()
|
||||
if data, ok := n.pages[unique]; ok {
|
||||
suffix := ".md"
|
||||
if !markdown {
|
||||
suffix = ".out"
|
||||
}
|
||||
page := data.(gin.H)
|
||||
os.Remove(path.Join(n.storage, unique, eunique+suffix))
|
||||
|
||||
lines := page["lines"].([]interface{})
|
||||
if idx := findLineByKey(lines, "unique", eunique); idx > -1 {
|
||||
page["lines"] = append(lines[:idx], lines[idx+1:]...)
|
||||
}
|
||||
|
||||
filepath := path.Join(n.storage, unique, PAGE_FILE)
|
||||
bytes, _ := json.MarshalIndent(page, "", "\t")
|
||||
if ioutil.WriteFile(filepath, bytes, 0644) == nil {
|
||||
n.pages[unique] = page
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (n *Notebook) new(title, filename, binary string) string {
|
||||
if len(title) < 1 {
|
||||
return ""
|
||||
info, err := RizinInfo(n.rizin)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
n.mutex.Lock()
|
||||
defer n.mutex.Unlock()
|
||||
var unique = Nonce(PAGE_NONCE_SIZE)
|
||||
for {
|
||||
if _, ok := n.pages[unique]; !ok {
|
||||
break
|
||||
}
|
||||
unique = Nonce(PAGE_NONCE_SIZE)
|
||||
}
|
||||
data := gin.H{
|
||||
"title": title,
|
||||
"unique": unique,
|
||||
"filename": filename,
|
||||
"binary": binary,
|
||||
"lines": []interface{}{},
|
||||
}
|
||||
if err := os.MkdirAll(path.Join(n.storage, unique), os.ModePerm); err != nil {
|
||||
return ""
|
||||
}
|
||||
filepath := path.Join(n.storage, unique, PAGE_FILE)
|
||||
bytes, _ := json.MarshalIndent(data, "", "\t")
|
||||
if err := ioutil.WriteFile(filepath, bytes, 0644); err != nil {
|
||||
return ""
|
||||
}
|
||||
n.pages[unique] = data
|
||||
n.uniques = append(n.uniques, unique)
|
||||
sort.Strings(n.uniques)
|
||||
return unique
|
||||
}
|
||||
|
||||
func (n *Notebook) rename(unique, title string) bool {
|
||||
if len(title) < 1 || len(unique) != PAGE_NONCE_SIZE {
|
||||
return false
|
||||
}
|
||||
n.mutex.Lock()
|
||||
defer n.mutex.Unlock()
|
||||
|
||||
if data, ok := n.pages[unique]; ok {
|
||||
page := data.(gin.H)
|
||||
page["title"] = title
|
||||
filepath := path.Join(n.storage, unique, PAGE_FILE)
|
||||
bytes, _ := json.MarshalIndent(page, "", "\t")
|
||||
if ioutil.WriteFile(filepath, bytes, 0644) == nil {
|
||||
n.pages[unique] = page
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (n *Notebook) delete(unique string) bool {
|
||||
if len(unique) != PAGE_NONCE_SIZE {
|
||||
return false
|
||||
}
|
||||
if _, ok := n.pages[unique]; !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
filepath := path.Join(n.storage, unique)
|
||||
if os.RemoveAll(filepath) == nil {
|
||||
delete(n.pages, unique)
|
||||
idx := findNonce(n.uniques, unique)
|
||||
n.uniques = append(n.uniques[:idx], n.uniques[idx+1:]...)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
n.rizinInfoCache = append([]string(nil), info...)
|
||||
n.mutex.Unlock()
|
||||
return info, nil
|
||||
}
|
||||
|
||||
// Returns existing pipe or opens a new one, extracting the binary to a temp file.
|
||||
func (n *Notebook) open(unique string, open bool) *Rizin {
|
||||
if len(unique) != PAGE_NONCE_SIZE {
|
||||
if !IsValidNonce(unique, PageNonceSize) {
|
||||
return nil
|
||||
}
|
||||
n.mutex.Lock()
|
||||
defer n.mutex.Unlock()
|
||||
|
||||
if p, ok := n.pipes[unique]; ok {
|
||||
return p
|
||||
}
|
||||
if p, ok := n.pages[unique]; ok && open {
|
||||
page := p.(gin.H)
|
||||
filepath := path.Join(n.storage, unique, page["binary"].(string))
|
||||
prjpath := path.Join(n.storage, unique, "project.rzdb")
|
||||
// This should be async.
|
||||
rizin := NewRizin(n.rizin, filepath, prjpath)
|
||||
if rizin == nil {
|
||||
return nil
|
||||
}
|
||||
n.pipes[unique] = rizin
|
||||
return rizin
|
||||
|
||||
if !open {
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
|
||||
page, err := catalog.GetPage(unique)
|
||||
if err != nil || page == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
binaryPath, err := catalog.GetBinaryForPipe(unique, n.storage)
|
||||
if err != nil {
|
||||
fmt.Printf("warning: failed to extract binary for pipe: %v\n", err)
|
||||
return nil
|
||||
}
|
||||
prjPath := filepath.Join(n.storage, unique, "project.rzdb")
|
||||
|
||||
rizin := NewRizin(n.rizin, binaryPath, prjPath)
|
||||
if rizin == nil {
|
||||
return nil
|
||||
}
|
||||
n.pipes[unique] = rizin
|
||||
return rizin
|
||||
}
|
||||
|
||||
func (n *Notebook) close(unique string) {
|
||||
if len(unique) != PAGE_NONCE_SIZE {
|
||||
func (n *Notebook) closePipe(unique string) {
|
||||
if !IsValidNonce(unique, PageNonceSize) {
|
||||
return
|
||||
}
|
||||
n.mutex.Lock()
|
||||
defer n.mutex.Unlock()
|
||||
if p, ok := n.pipes[unique]; ok {
|
||||
p.close()
|
||||
p, ok := n.pipes[unique]
|
||||
if ok {
|
||||
delete(n.pipes, unique)
|
||||
}
|
||||
}
|
||||
n.mutex.Unlock()
|
||||
|
||||
func (n *Notebook) exists(paths ...string) bool {
|
||||
args := append([]string{n.storage}, paths...)
|
||||
_, err := os.Stat(path.Join(args...))
|
||||
if os.IsNotExist(err) {
|
||||
return false
|
||||
if ok && p != nil {
|
||||
p.close()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (n *Notebook) file(paths ...string) ([]byte, error) {
|
||||
args := append([]string{n.storage}, paths...)
|
||||
filepath := path.Join(args...)
|
||||
return ioutil.ReadFile(filepath)
|
||||
}
|
||||
func (n *Notebook) closeAllPipes() {
|
||||
n.mutex.Lock()
|
||||
pipesToClose := make(map[string]*Rizin)
|
||||
for k, v := range n.pipes {
|
||||
pipesToClose[k] = v
|
||||
}
|
||||
n.pipes = map[string]*Rizin{}
|
||||
n.mutex.Unlock()
|
||||
|
||||
func (n *Notebook) save(bytes []byte, paths ...string) bool {
|
||||
args := append([]string{n.storage}, paths...)
|
||||
filepath := path.Join(args...)
|
||||
return ioutil.WriteFile(filepath, bytes, 0644) == nil
|
||||
for unique, p := range pipesToClose {
|
||||
fmt.Printf("Closing pipe for page %s...\n", unique)
|
||||
p.close()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,730 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
type NotebookSearchMode string
|
||||
|
||||
const (
|
||||
NotebookSearchModeLiteral NotebookSearchMode = "literal"
|
||||
NotebookSearchModeRegex NotebookSearchMode = "regex"
|
||||
NotebookSearchModeFuzzy NotebookSearchMode = "fuzzy"
|
||||
)
|
||||
|
||||
type NotebookSearchSurface string
|
||||
|
||||
const (
|
||||
NotebookSearchSurfaceContent NotebookSearchSurface = "content"
|
||||
NotebookSearchSurfaceOutput NotebookSearchSurface = "output"
|
||||
)
|
||||
|
||||
type NotebookSearchSurfaceFilter string
|
||||
|
||||
const (
|
||||
NotebookSearchSurfaceFilterAll NotebookSearchSurfaceFilter = "all"
|
||||
NotebookSearchSurfaceFilterContent NotebookSearchSurfaceFilter = "content"
|
||||
NotebookSearchSurfaceFilterOutput NotebookSearchSurfaceFilter = "output"
|
||||
)
|
||||
|
||||
type NotebookSearchCellTypeFilter string
|
||||
|
||||
const (
|
||||
NotebookSearchCellTypeFilterAll NotebookSearchCellTypeFilter = "all"
|
||||
NotebookSearchCellTypeFilterCommand NotebookSearchCellTypeFilter = "command"
|
||||
NotebookSearchCellTypeFilterScript NotebookSearchCellTypeFilter = "script"
|
||||
NotebookSearchCellTypeFilterMarkdown NotebookSearchCellTypeFilter = "markdown"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultNotebookSearchLimit = 200
|
||||
maxNotebookSearchLimit = 500
|
||||
fuzzyNoScore = -1 << 30
|
||||
fuzzyGapPenalty = 8
|
||||
)
|
||||
|
||||
type NotebookSearchOptions struct {
|
||||
Query string
|
||||
Mode NotebookSearchMode
|
||||
CaseSensitive bool
|
||||
Limit int
|
||||
SurfaceFilter NotebookSearchSurfaceFilter
|
||||
CellTypeFilter NotebookSearchCellTypeFilter
|
||||
}
|
||||
|
||||
type NotebookSearchRange struct {
|
||||
Start int `json:"start"`
|
||||
End int `json:"end"`
|
||||
}
|
||||
|
||||
type NotebookSearchResult struct {
|
||||
CellID string `json:"cell_id"`
|
||||
CellType string `json:"cell_type"`
|
||||
Surface NotebookSearchSurface `json:"surface"`
|
||||
CellPosition int `json:"cell_position"`
|
||||
LineNumber int `json:"line_number"`
|
||||
StartColumn int `json:"start_column"`
|
||||
EndColumn int `json:"end_column"`
|
||||
LineText string `json:"line_text"`
|
||||
Ranges []NotebookSearchRange `json:"ranges"`
|
||||
Score int `json:"score,omitempty"`
|
||||
}
|
||||
|
||||
type NotebookSearchResponse struct {
|
||||
Query string `json:"query"`
|
||||
Mode NotebookSearchMode `json:"mode"`
|
||||
CaseSensitive bool `json:"case_sensitive"`
|
||||
SurfaceFilter NotebookSearchSurfaceFilter `json:"surface_filter"`
|
||||
CellTypeFilter NotebookSearchCellTypeFilter `json:"cell_type_filter"`
|
||||
Total int `json:"total"`
|
||||
Truncated bool `json:"truncated"`
|
||||
Results []NotebookSearchResult `json:"results"`
|
||||
}
|
||||
|
||||
type notebookSearchDocument struct {
|
||||
CellID string
|
||||
CellType string
|
||||
Surface NotebookSearchSurface
|
||||
CellPosition int
|
||||
Text string
|
||||
}
|
||||
|
||||
type notebookSearchLineMatch struct {
|
||||
Ranges []NotebookSearchRange
|
||||
Score int
|
||||
}
|
||||
|
||||
func SearchNotebook(cells []CellRow, options NotebookSearchOptions) (NotebookSearchResponse, error) {
|
||||
mode, err := normalizeNotebookSearchMode(options.Mode)
|
||||
if err != nil {
|
||||
return NotebookSearchResponse{}, err
|
||||
}
|
||||
|
||||
surfaceFilter, err := normalizeNotebookSearchSurfaceFilter(options.SurfaceFilter)
|
||||
if err != nil {
|
||||
return NotebookSearchResponse{}, err
|
||||
}
|
||||
|
||||
cellTypeFilter, err := normalizeNotebookSearchCellTypeFilter(options.CellTypeFilter)
|
||||
if err != nil {
|
||||
return NotebookSearchResponse{}, err
|
||||
}
|
||||
|
||||
query := strings.TrimSpace(options.Query)
|
||||
limit := normalizeNotebookSearchLimit(options.Limit)
|
||||
response := NotebookSearchResponse{
|
||||
Query: query,
|
||||
Mode: mode,
|
||||
CaseSensitive: options.CaseSensitive,
|
||||
SurfaceFilter: surfaceFilter,
|
||||
CellTypeFilter: cellTypeFilter,
|
||||
Results: []NotebookSearchResult{},
|
||||
}
|
||||
if query == "" {
|
||||
return response, nil
|
||||
}
|
||||
|
||||
docs := buildNotebookSearchDocuments(cells, surfaceFilter, cellTypeFilter)
|
||||
results := make([]NotebookSearchResult, 0, 32)
|
||||
|
||||
switch mode {
|
||||
case NotebookSearchModeLiteral:
|
||||
results = searchNotebookLiteral(docs, query, options.CaseSensitive)
|
||||
case NotebookSearchModeRegex:
|
||||
results, err = searchNotebookRegex(docs, query, options.CaseSensitive)
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
case NotebookSearchModeFuzzy:
|
||||
results = searchNotebookFuzzy(docs, query, options.CaseSensitive)
|
||||
default:
|
||||
return response, fmt.Errorf("unsupported search mode: %s", mode)
|
||||
}
|
||||
|
||||
response.Total = len(results)
|
||||
if len(results) > limit {
|
||||
response.Truncated = true
|
||||
results = results[:limit]
|
||||
}
|
||||
response.Results = results
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func normalizeNotebookSearchMode(mode NotebookSearchMode) (NotebookSearchMode, error) {
|
||||
if mode == "" {
|
||||
return NotebookSearchModeLiteral, nil
|
||||
}
|
||||
switch mode {
|
||||
case NotebookSearchModeLiteral, NotebookSearchModeRegex, NotebookSearchModeFuzzy:
|
||||
return mode, nil
|
||||
default:
|
||||
return "", fmt.Errorf("invalid search mode: %s", mode)
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeNotebookSearchSurfaceFilter(filter NotebookSearchSurfaceFilter) (NotebookSearchSurfaceFilter, error) {
|
||||
if filter == "" {
|
||||
return NotebookSearchSurfaceFilterAll, nil
|
||||
}
|
||||
switch filter {
|
||||
case NotebookSearchSurfaceFilterAll, NotebookSearchSurfaceFilterContent, NotebookSearchSurfaceFilterOutput:
|
||||
return filter, nil
|
||||
default:
|
||||
return "", fmt.Errorf("invalid surface filter: %s", filter)
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeNotebookSearchCellTypeFilter(filter NotebookSearchCellTypeFilter) (NotebookSearchCellTypeFilter, error) {
|
||||
if filter == "" {
|
||||
return NotebookSearchCellTypeFilterAll, nil
|
||||
}
|
||||
switch filter {
|
||||
case NotebookSearchCellTypeFilterAll, NotebookSearchCellTypeFilterCommand, NotebookSearchCellTypeFilterScript, NotebookSearchCellTypeFilterMarkdown:
|
||||
return filter, nil
|
||||
default:
|
||||
return "", fmt.Errorf("invalid cell type filter: %s", filter)
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeNotebookSearchLimit(limit int) int {
|
||||
if limit <= 0 {
|
||||
return defaultNotebookSearchLimit
|
||||
}
|
||||
if limit > maxNotebookSearchLimit {
|
||||
return maxNotebookSearchLimit
|
||||
}
|
||||
return limit
|
||||
}
|
||||
|
||||
func buildNotebookSearchDocuments(cells []CellRow, surfaceFilter NotebookSearchSurfaceFilter, cellTypeFilter NotebookSearchCellTypeFilter) []notebookSearchDocument {
|
||||
docs := make([]notebookSearchDocument, 0, len(cells)*2)
|
||||
for _, cell := range cells {
|
||||
if !notebookSearchMatchesCellType(cell.Type, cellTypeFilter) {
|
||||
continue
|
||||
}
|
||||
switch cell.Type {
|
||||
case "markdown":
|
||||
if cell.Content != "" && notebookSearchMatchesSurface(NotebookSearchSurfaceContent, surfaceFilter) {
|
||||
docs = append(docs, notebookSearchDocument{
|
||||
CellID: cell.ID,
|
||||
CellType: cell.Type,
|
||||
Surface: NotebookSearchSurfaceContent,
|
||||
CellPosition: cell.Position,
|
||||
Text: normalizeNotebookSearchText(cell.Content),
|
||||
})
|
||||
}
|
||||
case "command", "script":
|
||||
if cell.Content != "" && notebookSearchMatchesSurface(NotebookSearchSurfaceContent, surfaceFilter) {
|
||||
docs = append(docs, notebookSearchDocument{
|
||||
CellID: cell.ID,
|
||||
CellType: cell.Type,
|
||||
Surface: NotebookSearchSurfaceContent,
|
||||
CellPosition: cell.Position,
|
||||
Text: normalizeNotebookSearchText(cell.Content),
|
||||
})
|
||||
}
|
||||
if len(cell.Output) > 0 && notebookSearchMatchesSurface(NotebookSearchSurfaceOutput, surfaceFilter) {
|
||||
docs = append(docs, notebookSearchDocument{
|
||||
CellID: cell.ID,
|
||||
CellType: cell.Type,
|
||||
Surface: NotebookSearchSurfaceOutput,
|
||||
CellPosition: cell.Position,
|
||||
Text: normalizeNotebookSearchText(outputVisibleText(string(cell.Output))),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return docs
|
||||
}
|
||||
|
||||
func notebookSearchMatchesSurface(surface NotebookSearchSurface, filter NotebookSearchSurfaceFilter) bool {
|
||||
return filter == NotebookSearchSurfaceFilterAll || NotebookSearchSurfaceFilter(surface) == filter
|
||||
}
|
||||
|
||||
func notebookSearchMatchesCellType(cellType string, filter NotebookSearchCellTypeFilter) bool {
|
||||
return filter == NotebookSearchCellTypeFilterAll || NotebookSearchCellTypeFilter(cellType) == filter
|
||||
}
|
||||
|
||||
func normalizeNotebookSearchText(text string) string {
|
||||
text = strings.ReplaceAll(text, "\r\n", "\n")
|
||||
text = strings.ReplaceAll(text, "\r", "\n")
|
||||
return text
|
||||
}
|
||||
|
||||
func searchNotebookLiteral(docs []notebookSearchDocument, query string, caseSensitive bool) []NotebookSearchResult {
|
||||
queryRunes := []rune(query)
|
||||
results := make([]NotebookSearchResult, 0, 32)
|
||||
for _, doc := range docs {
|
||||
lines := strings.Split(doc.Text, "\n")
|
||||
for lineIdx, line := range lines {
|
||||
matches := findLiteralLineMatches([]rune(line), queryRunes, caseSensitive)
|
||||
for _, match := range matches {
|
||||
results = append(results, notebookSearchResultFromLineMatch(doc, lineIdx+1, line, match))
|
||||
}
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
func searchNotebookRegex(docs []notebookSearchDocument, query string, caseSensitive bool) ([]NotebookSearchResult, error) {
|
||||
pattern := query
|
||||
if !caseSensitive {
|
||||
pattern = "(?i)" + query
|
||||
}
|
||||
re, err := regexp.Compile(pattern)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid regex: %w", err)
|
||||
}
|
||||
if re.MatchString("") {
|
||||
return nil, fmt.Errorf("regex must not match empty strings")
|
||||
}
|
||||
|
||||
results := make([]NotebookSearchResult, 0, 32)
|
||||
for _, doc := range docs {
|
||||
lines := strings.Split(doc.Text, "\n")
|
||||
for lineIdx, line := range lines {
|
||||
matches := findRegexLineMatches(line, re)
|
||||
for _, match := range matches {
|
||||
results = append(results, notebookSearchResultFromLineMatch(doc, lineIdx+1, line, match))
|
||||
}
|
||||
}
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func searchNotebookFuzzy(docs []notebookSearchDocument, query string, caseSensitive bool) []NotebookSearchResult {
|
||||
results := make([]NotebookSearchResult, 0, 32)
|
||||
for _, doc := range docs {
|
||||
lines := strings.Split(doc.Text, "\n")
|
||||
for lineIdx, line := range lines {
|
||||
match, ok := findFuzzyLineMatch(line, query, caseSensitive)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
results = append(results, notebookSearchResultFromLineMatch(doc, lineIdx+1, line, match))
|
||||
}
|
||||
}
|
||||
|
||||
sort.SliceStable(results, func(i, j int) bool {
|
||||
if results[i].Score != results[j].Score {
|
||||
return results[i].Score > results[j].Score
|
||||
}
|
||||
spanI := results[i].EndColumn - results[i].StartColumn
|
||||
spanJ := results[j].EndColumn - results[j].StartColumn
|
||||
if spanI != spanJ {
|
||||
return spanI < spanJ
|
||||
}
|
||||
if results[i].CellPosition != results[j].CellPosition {
|
||||
return results[i].CellPosition < results[j].CellPosition
|
||||
}
|
||||
if results[i].LineNumber != results[j].LineNumber {
|
||||
return results[i].LineNumber < results[j].LineNumber
|
||||
}
|
||||
return results[i].StartColumn < results[j].StartColumn
|
||||
})
|
||||
return results
|
||||
}
|
||||
|
||||
func notebookSearchResultFromLineMatch(doc notebookSearchDocument, lineNumber int, line string, match notebookSearchLineMatch) NotebookSearchResult {
|
||||
startCol, endCol := notebookSearchRangeColumns(match.Ranges)
|
||||
return NotebookSearchResult{
|
||||
CellID: doc.CellID,
|
||||
CellType: doc.CellType,
|
||||
Surface: doc.Surface,
|
||||
CellPosition: doc.CellPosition,
|
||||
LineNumber: lineNumber,
|
||||
StartColumn: startCol,
|
||||
EndColumn: endCol,
|
||||
LineText: line,
|
||||
Ranges: match.Ranges,
|
||||
Score: match.Score,
|
||||
}
|
||||
}
|
||||
|
||||
func notebookSearchRangeColumns(ranges []NotebookSearchRange) (int, int) {
|
||||
if len(ranges) == 0 {
|
||||
return 0, 0
|
||||
}
|
||||
start := ranges[0].Start + 1
|
||||
end := ranges[0].End
|
||||
for _, r := range ranges[1:] {
|
||||
if r.Start+1 < start {
|
||||
start = r.Start + 1
|
||||
}
|
||||
if r.End > end {
|
||||
end = r.End
|
||||
}
|
||||
}
|
||||
return start, end
|
||||
}
|
||||
|
||||
func findLiteralLineMatches(lineRunes, queryRunes []rune, caseSensitive bool) []notebookSearchLineMatch {
|
||||
if len(lineRunes) == 0 || len(queryRunes) == 0 || len(queryRunes) > len(lineRunes) {
|
||||
return nil
|
||||
}
|
||||
|
||||
matches := make([]notebookSearchLineMatch, 0, 2)
|
||||
for idx := 0; idx <= len(lineRunes)-len(queryRunes); idx++ {
|
||||
if literalRunesEqual(lineRunes[idx:idx+len(queryRunes)], queryRunes, caseSensitive) {
|
||||
matches = append(matches, notebookSearchLineMatch{
|
||||
Ranges: []NotebookSearchRange{{Start: idx, End: idx + len(queryRunes)}},
|
||||
Score: len(queryRunes) * 100,
|
||||
})
|
||||
}
|
||||
}
|
||||
return matches
|
||||
}
|
||||
|
||||
func literalRunesEqual(line, query []rune, caseSensitive bool) bool {
|
||||
if len(line) != len(query) {
|
||||
return false
|
||||
}
|
||||
for i := range query {
|
||||
if !searchRunesEqual(line[i], query[i], caseSensitive) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func searchRunesEqual(a, b rune, caseSensitive bool) bool {
|
||||
if caseSensitive {
|
||||
return a == b
|
||||
}
|
||||
return unicode.ToLower(a) == unicode.ToLower(b)
|
||||
}
|
||||
|
||||
func findRegexLineMatches(line string, re *regexp.Regexp) []notebookSearchLineMatch {
|
||||
byteMatches := re.FindAllStringIndex(line, -1)
|
||||
if len(byteMatches) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
results := make([]notebookSearchLineMatch, 0, len(byteMatches))
|
||||
for _, byteMatch := range byteMatches {
|
||||
start := byteOffsetToRuneIndex(line, byteMatch[0])
|
||||
end := byteOffsetToRuneIndex(line, byteMatch[1])
|
||||
if end <= start {
|
||||
continue
|
||||
}
|
||||
results = append(results, notebookSearchLineMatch{
|
||||
Ranges: []NotebookSearchRange{{Start: start, End: end}},
|
||||
Score: (end - start) * 100,
|
||||
})
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
func byteOffsetToRuneIndex(text string, byteOffset int) int {
|
||||
if byteOffset <= 0 {
|
||||
return 0
|
||||
}
|
||||
if byteOffset >= len(text) {
|
||||
return utf8.RuneCountInString(text)
|
||||
}
|
||||
return utf8.RuneCountInString(text[:byteOffset])
|
||||
}
|
||||
|
||||
func findFuzzyLineMatch(line, query string, caseSensitive bool) (notebookSearchLineMatch, bool) {
|
||||
lineRunes := []rune(line)
|
||||
if len(lineRunes) == 0 {
|
||||
return notebookSearchLineMatch{}, false
|
||||
}
|
||||
|
||||
tokens := fuzzyQueryTokens(query)
|
||||
if len(tokens) == 0 {
|
||||
return notebookSearchLineMatch{}, false
|
||||
}
|
||||
|
||||
allPositions := make([]int, 0, len([]rune(query)))
|
||||
totalScore := 0
|
||||
searchStart := 0
|
||||
for _, token := range tokens {
|
||||
tokenRunes := []rune(token)
|
||||
match, ok := bestFuzzyTokenMatch(lineRunes, tokenRunes, searchStart, caseSensitive)
|
||||
if !ok {
|
||||
return notebookSearchLineMatch{}, false
|
||||
}
|
||||
allPositions = append(allPositions, match.positions...)
|
||||
totalScore += match.score
|
||||
searchStart = match.positions[len(match.positions)-1] + 1
|
||||
}
|
||||
|
||||
if len(allPositions) == 0 {
|
||||
return notebookSearchLineMatch{}, false
|
||||
}
|
||||
|
||||
sort.Ints(allPositions)
|
||||
ranges := mergeFuzzyPositions(allPositions)
|
||||
if len(ranges) == 0 {
|
||||
return notebookSearchLineMatch{}, false
|
||||
}
|
||||
|
||||
return notebookSearchLineMatch{
|
||||
Ranges: ranges,
|
||||
Score: totalScore,
|
||||
}, true
|
||||
}
|
||||
|
||||
type fuzzyTokenMatch struct {
|
||||
positions []int
|
||||
score int
|
||||
}
|
||||
|
||||
func fuzzyQueryTokens(query string) []string {
|
||||
return strings.Fields(strings.TrimSpace(query))
|
||||
}
|
||||
|
||||
func bestFuzzyTokenMatch(lineRunes, tokenRunes []rune, searchStart int, caseSensitive bool) (fuzzyTokenMatch, bool) {
|
||||
if len(tokenRunes) == 0 || searchStart >= len(lineRunes) {
|
||||
return fuzzyTokenMatch{}, false
|
||||
}
|
||||
|
||||
lineLen := len(lineRunes)
|
||||
tokenLen := len(tokenRunes)
|
||||
parents := make([][]int, tokenLen)
|
||||
for idx := range parents {
|
||||
parents[idx] = make([]int, lineLen)
|
||||
for col := range parents[idx] {
|
||||
parents[idx][col] = -1
|
||||
}
|
||||
}
|
||||
|
||||
prevScores := make([]int, lineLen)
|
||||
prevStarts := make([]int, lineLen)
|
||||
currScores := make([]int, lineLen)
|
||||
currStarts := make([]int, lineLen)
|
||||
fillFuzzyState(prevScores, prevStarts)
|
||||
fillFuzzyState(currScores, currStarts)
|
||||
|
||||
for col := searchStart; col < lineLen; col++ {
|
||||
if !searchRunesEqual(lineRunes[col], tokenRunes[0], caseSensitive) {
|
||||
continue
|
||||
}
|
||||
prevScores[col] = fuzzyBaseScore(lineRunes, tokenRunes[0], col, caseSensitive)
|
||||
prevStarts[col] = col
|
||||
}
|
||||
|
||||
for row := 1; row < tokenLen; row++ {
|
||||
fillFuzzyState(currScores, currStarts)
|
||||
bestGapScore := fuzzyNoScore
|
||||
bestGapStart := -1
|
||||
bestGapIndex := -1
|
||||
for col := searchStart; col < lineLen; col++ {
|
||||
gapCandidate := col - 2
|
||||
if gapCandidate >= searchStart && prevScores[gapCandidate] != fuzzyNoScore {
|
||||
score := prevScores[gapCandidate] + fuzzyGapPenalty*(gapCandidate+1)
|
||||
if fuzzyStateBetter(score, prevStarts[gapCandidate], gapCandidate, bestGapScore, bestGapStart, bestGapIndex) {
|
||||
bestGapScore = score
|
||||
bestGapStart = prevStarts[gapCandidate]
|
||||
bestGapIndex = gapCandidate
|
||||
}
|
||||
}
|
||||
|
||||
if !searchRunesEqual(lineRunes[col], tokenRunes[row], caseSensitive) {
|
||||
continue
|
||||
}
|
||||
|
||||
bestScore := fuzzyNoScore
|
||||
bestStart := -1
|
||||
bestPrev := -1
|
||||
|
||||
if col-1 >= searchStart && prevScores[col-1] != fuzzyNoScore {
|
||||
score := prevScores[col-1] + fuzzyCharScore(lineRunes, tokenRunes[row], col, true, caseSensitive)
|
||||
if fuzzyStateBetter(score, prevStarts[col-1], col-1, bestScore, bestStart, bestPrev) {
|
||||
bestScore = score
|
||||
bestStart = prevStarts[col-1]
|
||||
bestPrev = col - 1
|
||||
}
|
||||
}
|
||||
|
||||
if bestGapScore != fuzzyNoScore {
|
||||
score := bestGapScore - fuzzyGapPenalty*col + fuzzyCharScore(lineRunes, tokenRunes[row], col, false, caseSensitive)
|
||||
if fuzzyStateBetter(score, bestGapStart, bestGapIndex, bestScore, bestStart, bestPrev) {
|
||||
bestScore = score
|
||||
bestStart = bestGapStart
|
||||
bestPrev = bestGapIndex
|
||||
}
|
||||
}
|
||||
|
||||
if bestScore == fuzzyNoScore {
|
||||
continue
|
||||
}
|
||||
|
||||
currScores[col] = bestScore
|
||||
currStarts[col] = bestStart
|
||||
parents[row][col] = bestPrev
|
||||
}
|
||||
|
||||
prevScores, currScores = currScores, prevScores
|
||||
prevStarts, currStarts = currStarts, prevStarts
|
||||
}
|
||||
|
||||
bestEnd := -1
|
||||
bestScore := fuzzyNoScore
|
||||
bestStart := -1
|
||||
for col := searchStart; col < lineLen; col++ {
|
||||
if prevScores[col] == fuzzyNoScore {
|
||||
continue
|
||||
}
|
||||
score := finalizeFuzzyScore(prevScores[col], prevStarts[col], col, tokenLen)
|
||||
if fuzzyFinalStateBetter(score, prevStarts[col], col, bestScore, bestStart, bestEnd) {
|
||||
bestScore = score
|
||||
bestStart = prevStarts[col]
|
||||
bestEnd = col
|
||||
}
|
||||
}
|
||||
|
||||
if bestEnd < 0 {
|
||||
return fuzzyTokenMatch{}, false
|
||||
}
|
||||
|
||||
positions := make([]int, tokenLen)
|
||||
col := bestEnd
|
||||
for row := tokenLen - 1; row >= 0; row-- {
|
||||
positions[row] = col
|
||||
if row > 0 {
|
||||
col = parents[row][col]
|
||||
if col < 0 {
|
||||
return fuzzyTokenMatch{}, false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fuzzyTokenMatch{positions: positions, score: bestScore}, true
|
||||
}
|
||||
|
||||
func fillFuzzyState(scores, starts []int) {
|
||||
for idx := range scores {
|
||||
scores[idx] = fuzzyNoScore
|
||||
starts[idx] = -1
|
||||
}
|
||||
}
|
||||
|
||||
func fuzzyBaseScore(lineRunes []rune, tokenRune rune, pos int, caseSensitive bool) int {
|
||||
score := 60 - pos*2
|
||||
score += fuzzyPositionBonus(lineRunes, pos)
|
||||
if !caseSensitive && lineRunes[pos] == tokenRune {
|
||||
score += 8
|
||||
}
|
||||
return score
|
||||
}
|
||||
|
||||
func fuzzyCharScore(lineRunes []rune, tokenRune rune, pos int, adjacent bool, caseSensitive bool) int {
|
||||
score := 44
|
||||
if adjacent {
|
||||
score += 56
|
||||
}
|
||||
score += fuzzyPositionBonus(lineRunes, pos)
|
||||
if !caseSensitive && lineRunes[pos] == tokenRune {
|
||||
score += 8
|
||||
}
|
||||
return score
|
||||
}
|
||||
|
||||
func finalizeFuzzyScore(score, start, end, tokenLen int) int {
|
||||
span := end - start + 1
|
||||
score -= span * 2
|
||||
if span == tokenLen {
|
||||
score += 60
|
||||
}
|
||||
if span <= tokenLen+2 {
|
||||
score += 18
|
||||
}
|
||||
if start == 0 {
|
||||
score += 10
|
||||
}
|
||||
return score
|
||||
}
|
||||
|
||||
func fuzzyPositionBonus(lineRunes []rune, pos int) int {
|
||||
score := 0
|
||||
if pos == 0 {
|
||||
score += 20
|
||||
}
|
||||
if fuzzyWordBoundary(lineRunes, pos) {
|
||||
score += 18
|
||||
}
|
||||
if fuzzyCamelBoundary(lineRunes, pos) {
|
||||
score += 12
|
||||
}
|
||||
return score
|
||||
}
|
||||
|
||||
func fuzzyWordBoundary(lineRunes []rune, pos int) bool {
|
||||
if pos <= 0 {
|
||||
return true
|
||||
}
|
||||
return !fuzzyIsWordRune(lineRunes[pos-1])
|
||||
}
|
||||
|
||||
func fuzzyCamelBoundary(lineRunes []rune, pos int) bool {
|
||||
if pos <= 0 {
|
||||
return false
|
||||
}
|
||||
prev := lineRunes[pos-1]
|
||||
curr := lineRunes[pos]
|
||||
if unicode.IsLower(prev) && unicode.IsUpper(curr) {
|
||||
return true
|
||||
}
|
||||
return unicode.IsDigit(prev) != unicode.IsDigit(curr) && fuzzyIsWordRune(prev) && fuzzyIsWordRune(curr)
|
||||
}
|
||||
|
||||
func fuzzyIsWordRune(r rune) bool {
|
||||
return unicode.IsLetter(r) || unicode.IsDigit(r)
|
||||
}
|
||||
|
||||
func fuzzyStateBetter(score, start, end, bestScore, bestStart, bestEnd int) bool {
|
||||
if score != bestScore {
|
||||
return score > bestScore
|
||||
}
|
||||
span := end - start
|
||||
bestSpan := bestEnd - bestStart
|
||||
if span != bestSpan {
|
||||
return span < bestSpan
|
||||
}
|
||||
if start != bestStart {
|
||||
return start < bestStart
|
||||
}
|
||||
return end < bestEnd
|
||||
}
|
||||
|
||||
func fuzzyFinalStateBetter(score, start, end, bestScore, bestStart, bestEnd int) bool {
|
||||
if score != bestScore {
|
||||
return score > bestScore
|
||||
}
|
||||
span := end - start
|
||||
bestSpan := bestEnd - bestStart
|
||||
if span != bestSpan {
|
||||
return span < bestSpan
|
||||
}
|
||||
if start != bestStart {
|
||||
return start < bestStart
|
||||
}
|
||||
return end < bestEnd
|
||||
}
|
||||
|
||||
func mergeFuzzyPositions(positions []int) []NotebookSearchRange {
|
||||
if len(positions) == 0 {
|
||||
return nil
|
||||
}
|
||||
ranges := make([]NotebookSearchRange, 0, len(positions))
|
||||
current := NotebookSearchRange{Start: positions[0], End: positions[0] + 1}
|
||||
for _, pos := range positions[1:] {
|
||||
if pos <= current.End {
|
||||
if pos+1 > current.End {
|
||||
current.End = pos + 1
|
||||
}
|
||||
continue
|
||||
}
|
||||
ranges = append(ranges, current)
|
||||
current = NotebookSearchRange{Start: pos, End: pos + 1}
|
||||
}
|
||||
ranges = append(ranges, current)
|
||||
return ranges
|
||||
}
|
||||
@@ -1,12 +1,17 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"html"
|
||||
"io"
|
||||
"math"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// tagMap maps ANSI SGR attribute codes to their HTML tag equivalents.
|
||||
var tagMap = map[string]string{
|
||||
"1": "b",
|
||||
"2": "b",
|
||||
@@ -16,6 +21,7 @@ var tagMap = map[string]string{
|
||||
"9": "strike",
|
||||
}
|
||||
|
||||
// colMap maps ANSI 8-color codes to CSS color names.
|
||||
var colMap = map[string]string{
|
||||
"0": "white",
|
||||
"1": "red",
|
||||
@@ -28,7 +34,24 @@ var colMap = map[string]string{
|
||||
"9": "white",
|
||||
}
|
||||
|
||||
var color265 = map[string]string{
|
||||
// foregroundColorMap maps ANSI 8-color codes to contrast-safe colors for text
|
||||
// rendered on the notebook output background.
|
||||
var foregroundColorMap = map[string]string{
|
||||
"0": "#d0d7de",
|
||||
"1": "#ff9e64",
|
||||
"2": "#98c379",
|
||||
"3": "#e5c07b",
|
||||
"4": "#61afef",
|
||||
"5": "#c678dd",
|
||||
"6": "#56b6c2",
|
||||
"7": "#f0f0f0",
|
||||
"9": "#ffffff",
|
||||
}
|
||||
|
||||
const outputBackgroundColor = "#4b4b4b"
|
||||
|
||||
// color256 maps ANSI 256-color palette indices to hex color codes.
|
||||
var color256 = map[string]string{
|
||||
"0": "#000000",
|
||||
"1": "#800000",
|
||||
"2": "#008000",
|
||||
@@ -287,37 +310,170 @@ var color265 = map[string]string{
|
||||
"255": "#eeeeee",
|
||||
}
|
||||
|
||||
var col256 = regexp.MustCompile(`^\[([34]8);5;(\d+)m`)
|
||||
var colrgb = regexp.MustCompile(`^\[([34]8);2;(\d+);(\d+);(\d+)m`)
|
||||
var escape = regexp.MustCompile(`^\[(\d+;)?(\d+;)?(\d+;)?(\d+;)?(\d+)([A-Za-z])`)
|
||||
// Precompiled regular expressions for ANSI escape sequence parsing.
|
||||
var (
|
||||
// col256Re matches 256-color ANSI escape sequences: ESC[38;5;Nm or ESC[48;5;Nm.
|
||||
col256Re = regexp.MustCompile(`^\[([34]8);5;(\d+)m`)
|
||||
|
||||
func toHtml(raw []byte) []byte {
|
||||
var output = strings.TrimSuffix(string(raw), "\x00")
|
||||
output = strings.ReplaceAll(output, "\r", "")
|
||||
output = strings.Trim(output, "\n")
|
||||
// colRGBRe matches RGB true color ANSI escape sequences: ESC[38;2;R;G;Bm.
|
||||
colRGBRe = regexp.MustCompile(`^\[([34]8|1);2;(\d+);(\d+);(\d+)m`)
|
||||
|
||||
// escapeRe matches generic ANSI escape sequences with numeric parameters.
|
||||
escapeRe = regexp.MustCompile(`^\[(;?\d+)+([A-Za-z])`)
|
||||
|
||||
outputHTMLTagRe = regexp.MustCompile(`(?s)<[^>]+>`)
|
||||
)
|
||||
|
||||
func parseHexColor(hexColor string) (int, int, int, bool) {
|
||||
if len(hexColor) != 7 || hexColor[0] != '#' {
|
||||
return 0, 0, 0, false
|
||||
}
|
||||
r, err := strconv.ParseInt(hexColor[1:3], 16, 0)
|
||||
if err != nil {
|
||||
return 0, 0, 0, false
|
||||
}
|
||||
g, err := strconv.ParseInt(hexColor[3:5], 16, 0)
|
||||
if err != nil {
|
||||
return 0, 0, 0, false
|
||||
}
|
||||
b, err := strconv.ParseInt(hexColor[5:7], 16, 0)
|
||||
if err != nil {
|
||||
return 0, 0, 0, false
|
||||
}
|
||||
return int(r), int(g), int(b), true
|
||||
}
|
||||
|
||||
func formatHexColor(red, green, blue int) string {
|
||||
if red < 0 {
|
||||
red = 0
|
||||
} else if red > 255 {
|
||||
red = 255
|
||||
}
|
||||
if green < 0 {
|
||||
green = 0
|
||||
} else if green > 255 {
|
||||
green = 255
|
||||
}
|
||||
if blue < 0 {
|
||||
blue = 0
|
||||
} else if blue > 255 {
|
||||
blue = 255
|
||||
}
|
||||
return fmt.Sprintf("#%02x%02x%02x", red, green, blue)
|
||||
}
|
||||
|
||||
func channelLuminance(value int) float64 {
|
||||
v := float64(value) / 255.0
|
||||
if v <= 0.03928 {
|
||||
return v / 12.92
|
||||
}
|
||||
return math.Pow((v+0.055)/1.055, 2.4)
|
||||
}
|
||||
|
||||
func relativeLuminance(red, green, blue int) float64 {
|
||||
return 0.2126*channelLuminance(red) + 0.7152*channelLuminance(green) + 0.0722*channelLuminance(blue)
|
||||
}
|
||||
|
||||
func contrastRatio(colorA, colorB string) float64 {
|
||||
r1, g1, b1, ok := parseHexColor(colorA)
|
||||
if !ok {
|
||||
return 21.0
|
||||
}
|
||||
r2, g2, b2, ok := parseHexColor(colorB)
|
||||
if !ok {
|
||||
return 21.0
|
||||
}
|
||||
l1 := relativeLuminance(r1, g1, b1)
|
||||
l2 := relativeLuminance(r2, g2, b2)
|
||||
if l1 < l2 {
|
||||
l1, l2 = l2, l1
|
||||
}
|
||||
return (l1 + 0.05) / (l2 + 0.05)
|
||||
}
|
||||
|
||||
func ensureReadableForeground(hexColor string) string {
|
||||
red, green, blue, ok := parseHexColor(hexColor)
|
||||
if !ok {
|
||||
return hexColor
|
||||
}
|
||||
adjusted := hexColor
|
||||
for i := 0; i < 12 && contrastRatio(adjusted, outputBackgroundColor) < 4.5; i++ {
|
||||
red += (255 - red) * 35 / 100
|
||||
green += (255 - green) * 35 / 100
|
||||
blue += (255 - blue) * 35 / 100
|
||||
adjusted = formatHexColor(red, green, blue)
|
||||
}
|
||||
return adjusted
|
||||
}
|
||||
|
||||
func toCsv(output string) ([]byte, error) {
|
||||
reader := csv.NewReader(strings.NewReader(output))
|
||||
reader.Comma = ','
|
||||
|
||||
var sb strings.Builder
|
||||
sb.WriteString("<table>\n")
|
||||
|
||||
for i := 0; ; i++ {
|
||||
record, err := reader.Read()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if i == 0 && len(record) < 2 {
|
||||
return nil, fmt.Errorf("CSV requires at least 2 columns, got %d", len(record))
|
||||
}
|
||||
|
||||
sb.WriteString("<tr>")
|
||||
for _, elem := range record {
|
||||
sb.WriteString("\n<td>")
|
||||
sb.WriteString(html.EscapeString(elem))
|
||||
sb.WriteString("</td>")
|
||||
}
|
||||
sb.WriteString("\n</tr>\n")
|
||||
}
|
||||
|
||||
sb.WriteString("</table>")
|
||||
return []byte(sb.String()), nil
|
||||
}
|
||||
|
||||
func toHtml(output string) []byte {
|
||||
if output == "" {
|
||||
return []byte{}
|
||||
}
|
||||
|
||||
// HTML-escape the raw text first.
|
||||
output = strings.ReplaceAll(output, "&", "&")
|
||||
output = strings.ReplaceAll(output, "<", "<")
|
||||
output = strings.ReplaceAll(output, ">", ">")
|
||||
output = strings.ReplaceAll(output, "\"", """)
|
||||
output = strings.ReplaceAll(output, "'", "'")
|
||||
var html = ""
|
||||
|
||||
var sb strings.Builder
|
||||
tokens := strings.Split(output, "\x1b")
|
||||
|
||||
for _, token := range tokens {
|
||||
if token == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle cursor control and screen-clearing codes (strip them).
|
||||
if strings.HasPrefix(token, "[H") ||
|
||||
strings.HasPrefix(token, "[s") ||
|
||||
strings.HasPrefix(token, "[u") ||
|
||||
strings.HasPrefix(token, "[J") ||
|
||||
strings.HasPrefix(token, "[K") {
|
||||
html += token[2:]
|
||||
} else if strings.HasPrefix(token, "[#;#R") {
|
||||
html += token[5:]
|
||||
} else if strings.HasPrefix(token, "[#") ||
|
||||
sb.WriteString(token[2:])
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(token, "[#;#R") {
|
||||
sb.WriteString(token[5:])
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(token, "[#") ||
|
||||
strings.HasPrefix(token, "[0m") ||
|
||||
strings.HasPrefix(token, "[0J") ||
|
||||
strings.HasPrefix(token, "[1J") ||
|
||||
@@ -326,128 +482,171 @@ func toHtml(raw []byte) []byte {
|
||||
strings.HasPrefix(token, "[1K") ||
|
||||
strings.HasPrefix(token, "[2K") ||
|
||||
strings.HasPrefix(token, "[6n") {
|
||||
html += token[3:]
|
||||
} else if strings.HasPrefix(token, "[1m") ||
|
||||
strings.HasPrefix(token, "[2m") {
|
||||
html += "<b>"
|
||||
html += token[3:]
|
||||
html += "</b>"
|
||||
} else if strings.HasPrefix(token, "[3m") {
|
||||
html += "<i>"
|
||||
html += token[3:]
|
||||
html += "</i>"
|
||||
} else if strings.HasPrefix(token, "[4m") {
|
||||
html += "<ins>"
|
||||
html += token[3:]
|
||||
html += "</ins>"
|
||||
} else if strings.HasPrefix(token, "[5m") {
|
||||
html += "<blink>"
|
||||
html += token[3:]
|
||||
html += "</blink>"
|
||||
} else if strings.HasPrefix(token, "[7m") ||
|
||||
strings.HasPrefix(token, "[8m") {
|
||||
html += token[3:]
|
||||
} else if strings.HasPrefix(token, "[9m") {
|
||||
html += "<strike>"
|
||||
html += token[3:]
|
||||
html += "</strike>"
|
||||
} else if strings.HasPrefix(token, "[22m") ||
|
||||
sb.WriteString(token[3:])
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle individual SGR attribute codes.
|
||||
if strings.HasPrefix(token, "[1m") || strings.HasPrefix(token, "[2m") {
|
||||
sb.WriteString("<b>")
|
||||
sb.WriteString(token[3:])
|
||||
sb.WriteString("</b>")
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(token, "[3m") {
|
||||
sb.WriteString("<i>")
|
||||
sb.WriteString(token[3:])
|
||||
sb.WriteString("</i>")
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(token, "[4m") {
|
||||
sb.WriteString("<ins>")
|
||||
sb.WriteString(token[3:])
|
||||
sb.WriteString("</ins>")
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(token, "[5m") {
|
||||
sb.WriteString("<blink>")
|
||||
sb.WriteString(token[3:])
|
||||
sb.WriteString("</blink>")
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(token, "[7m") || strings.HasPrefix(token, "[8m") {
|
||||
sb.WriteString(token[3:])
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(token, "[9m") {
|
||||
sb.WriteString("<strike>")
|
||||
sb.WriteString(token[3:])
|
||||
sb.WriteString("</strike>")
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle SGR reset codes (22m-29m).
|
||||
if strings.HasPrefix(token, "[22m") ||
|
||||
strings.HasPrefix(token, "[23m") ||
|
||||
strings.HasPrefix(token, "[24m") ||
|
||||
strings.HasPrefix(token, "[25m") ||
|
||||
strings.HasPrefix(token, "[28m") ||
|
||||
strings.HasPrefix(token, "[27m") ||
|
||||
strings.HasPrefix(token, "[28m") ||
|
||||
strings.HasPrefix(token, "[29m") {
|
||||
html += token[4:]
|
||||
} else if token == "7" || token == "8" {
|
||||
html += token[1:]
|
||||
} else {
|
||||
var btok = []byte(token)
|
||||
var found = col256.FindSubmatch(btok)
|
||||
if len(found) == 3 {
|
||||
if len(token) == len(found[0]) {
|
||||
continue
|
||||
}
|
||||
color, ok := color265[string(found[2])]
|
||||
if ok {
|
||||
if string(found[1]) == "48" {
|
||||
html += fmt.Sprintf("<span style=\"background-color: %s\">", color)
|
||||
} else {
|
||||
html += fmt.Sprintf("<span style=\"color: %s\">", color)
|
||||
}
|
||||
}
|
||||
html += token[len(found[0]):]
|
||||
if ok {
|
||||
html += "</span>"
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
found = colrgb.FindSubmatch(btok)
|
||||
if len(found) == 5 {
|
||||
if len(token) == len(found[0]) {
|
||||
continue
|
||||
}
|
||||
r, _ := strconv.Atoi(string(found[2]))
|
||||
g, _ := strconv.Atoi(string(found[3]))
|
||||
b, _ := strconv.Atoi(string(found[4]))
|
||||
r &= 0xFF
|
||||
g &= 0xFF
|
||||
b &= 0xFF
|
||||
if string(found[1]) == "48" {
|
||||
html += fmt.Sprintf("<span style=\"background-color: #%02x%02x%02x\">", r, g, b)
|
||||
} else {
|
||||
html += fmt.Sprintf("<span style=\"color: #%02x%02x%02x\">", r, g, b)
|
||||
}
|
||||
html += token[len(found[0]):]
|
||||
html += "</span>"
|
||||
continue
|
||||
}
|
||||
|
||||
found = escape.FindSubmatch(btok)
|
||||
if len(found) < 1 {
|
||||
html += token
|
||||
continue
|
||||
} else if string(found[len(found)-1]) != "m" {
|
||||
html += token[len(found[0]):]
|
||||
continue
|
||||
} else if len(token) == len(found[0]) {
|
||||
continue
|
||||
}
|
||||
var tags = ""
|
||||
for _, e := range found {
|
||||
e := string(e)
|
||||
if len(e) < 1 {
|
||||
continue
|
||||
}
|
||||
if e[len(e)-1] == ';' {
|
||||
e = e[:len(e)-1]
|
||||
}
|
||||
if e == "0" {
|
||||
html += tags
|
||||
tags = ""
|
||||
continue
|
||||
} else if tag, ok := tagMap[e]; ok {
|
||||
html += fmt.Sprintf("<%s>", tag)
|
||||
tags += fmt.Sprintf("</%s>", tag)
|
||||
} else if len(e) == 2 && (e[0] == '3' || e[0] == '4' || e[0] == '9') && e[1] != '8' {
|
||||
color, _ := colMap[string(e[1])]
|
||||
if e[0] == '4' {
|
||||
html += fmt.Sprintf("<span style=\"background-color: %s\">", color)
|
||||
} else {
|
||||
html += fmt.Sprintf("<span style=\"color: %s\">", color)
|
||||
}
|
||||
tags += "</span>"
|
||||
} else if len(e) == 3 && e[0] == '1' && e[1] == '0' && e[2] != '8' {
|
||||
color, _ := colMap[string(e[1])]
|
||||
html += fmt.Sprintf("<span style=\"background-color: %s\">", color)
|
||||
tags += "</span>"
|
||||
}
|
||||
}
|
||||
html += token[len(found[0]):]
|
||||
html += tags
|
||||
sb.WriteString(token[4:])
|
||||
continue
|
||||
}
|
||||
|
||||
if token == "7" || token == "8" {
|
||||
sb.WriteString(token[1:])
|
||||
continue
|
||||
}
|
||||
|
||||
// Try 256-color match.
|
||||
btok := []byte(token)
|
||||
found := col256Re.FindSubmatch(btok)
|
||||
if len(found) == 3 {
|
||||
if len(token) == len(found[0]) {
|
||||
continue
|
||||
}
|
||||
hexColor, ok := color256[string(found[2])]
|
||||
if ok {
|
||||
if string(found[1]) == "48" {
|
||||
sb.WriteString(fmt.Sprintf("<span style=\"background-color: %s\">", hexColor))
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprintf("<span style=\"color: %s\">", ensureReadableForeground(hexColor)))
|
||||
}
|
||||
}
|
||||
sb.WriteString(token[len(found[0]):])
|
||||
if ok {
|
||||
sb.WriteString("</span>")
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Try RGB true color match.
|
||||
found = colRGBRe.FindSubmatch(btok)
|
||||
if len(found) == 5 {
|
||||
if len(token) == len(found[0]) {
|
||||
continue
|
||||
}
|
||||
r, _ := strconv.Atoi(string(found[2]))
|
||||
g, _ := strconv.Atoi(string(found[3]))
|
||||
b, _ := strconv.Atoi(string(found[4]))
|
||||
r &= 0xFF
|
||||
g &= 0xFF
|
||||
b &= 0xFF
|
||||
if string(found[1]) == "48" {
|
||||
sb.WriteString(fmt.Sprintf("<span style=\"background-color: #%02x%02x%02x\">", r, g, b))
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprintf("<span style=\"color: %s\">", ensureReadableForeground(formatHexColor(r, g, b))))
|
||||
}
|
||||
sb.WriteString(token[len(found[0]):])
|
||||
sb.WriteString("</span>")
|
||||
continue
|
||||
}
|
||||
|
||||
// Try generic escape sequence match.
|
||||
found = escapeRe.FindSubmatch(btok)
|
||||
if len(found) < 1 {
|
||||
sb.WriteString(token)
|
||||
continue
|
||||
}
|
||||
if string(found[len(found)-1]) != "m" {
|
||||
sb.WriteString(token[len(found[0]):])
|
||||
continue
|
||||
}
|
||||
if len(token) == len(found[0]) {
|
||||
continue
|
||||
}
|
||||
|
||||
var tags strings.Builder
|
||||
for _, e := range found {
|
||||
es := string(e)
|
||||
if len(es) < 1 {
|
||||
continue
|
||||
}
|
||||
if es[len(es)-1] == ';' {
|
||||
es = es[:len(es)-1]
|
||||
}
|
||||
if es == "0" {
|
||||
sb.WriteString(tags.String())
|
||||
tags.Reset()
|
||||
continue
|
||||
}
|
||||
if tag, ok := tagMap[es]; ok {
|
||||
sb.WriteString(fmt.Sprintf("<%s>", tag))
|
||||
tags.WriteString(fmt.Sprintf("</%s>", tag))
|
||||
} else if len(es) == 2 && (es[0] == '3' || es[0] == '4' || es[0] == '9') && es[1] != '8' {
|
||||
clr, _ := colMap[string(es[1])]
|
||||
if es[0] == '4' {
|
||||
sb.WriteString(fmt.Sprintf("<span style=\"background-color: %s\">", clr))
|
||||
} else {
|
||||
fgColor, ok := foregroundColorMap[string(es[1])]
|
||||
if !ok {
|
||||
fgColor = clr
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("<span style=\"color: %s\">", fgColor))
|
||||
}
|
||||
tags.WriteString("</span>")
|
||||
} else if len(es) == 3 && es[0] == '1' && es[1] == '0' && es[2] != '8' {
|
||||
clr, _ := colMap[string(es[1])]
|
||||
sb.WriteString(fmt.Sprintf("<span style=\"background-color: %s\">", clr))
|
||||
tags.WriteString("</span>")
|
||||
}
|
||||
}
|
||||
sb.WriteString(token[len(found[0]):])
|
||||
sb.WriteString(tags.String())
|
||||
}
|
||||
html = strings.ReplaceAll(html, "\n", "<br>\n")
|
||||
return []byte(html)
|
||||
|
||||
result := strings.ReplaceAll(sb.String(), "\n", "<br>\n")
|
||||
return []byte("<pre>" + result + "</pre>")
|
||||
}
|
||||
|
||||
func outputVisibleText(output string) string {
|
||||
if output == "" {
|
||||
return ""
|
||||
}
|
||||
text := string(toHtml(output))
|
||||
text = strings.ReplaceAll(text, "<br>\n", "\n")
|
||||
text = strings.ReplaceAll(text, "<br>", "\n")
|
||||
text = outputHTMLTagRe.ReplaceAllString(text, "")
|
||||
return html.UnescapeString(text)
|
||||
}
|
||||
@@ -2,40 +2,145 @@ package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// pipeReadTimeout is the maximum time to wait for a response from Rizin.
|
||||
const pipeReadTimeout = 5 * time.Minute
|
||||
|
||||
type RizinCommandArg struct {
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
DefaultArg string `json:"default,omitempty"`
|
||||
Required bool `json:"required,omitempty"`
|
||||
IsOption bool `json:"is_option,omitempty"`
|
||||
IsArray bool `json:"is_array,omitempty"`
|
||||
Choices []string `json:"choices,omitempty"`
|
||||
}
|
||||
|
||||
type RizinCommandDetailEntry struct {
|
||||
Text string `json:"text,omitempty"`
|
||||
Comment string `json:"comment,omitempty"`
|
||||
Arg string `json:"arg_str,omitempty"`
|
||||
}
|
||||
|
||||
type RizinCommandDetail struct {
|
||||
Name string `json:"name"`
|
||||
Entries []RizinCommandDetailEntry `json:"entries,omitempty"`
|
||||
}
|
||||
|
||||
type RizinCommand struct {
|
||||
Command string `json:"cmd"`
|
||||
ArgsStr string `json:"args_str"`
|
||||
Args []RizinCommandArg `json:"args,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Summary string `json:"summary,omitempty"`
|
||||
Details []RizinCommandDetail `json:"details,omitempty"`
|
||||
}
|
||||
|
||||
type Rizin struct {
|
||||
pipe *exec.Cmd
|
||||
stdin io.WriteCloser
|
||||
stdout io.ReadCloser
|
||||
reader *bufio.Reader
|
||||
mutex sync.Mutex
|
||||
project string
|
||||
closed bool
|
||||
}
|
||||
|
||||
func RizinInfo(rizinbin string) ([]string, error) {
|
||||
out, err := exec.Command(rizinbin, "-version").Output()
|
||||
flags := [][]string{{"--version"}, {"-v"}}
|
||||
var lastErr error
|
||||
|
||||
for _, args := range flags {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
cmd := exec.CommandContext(ctx, rizinbin, args...)
|
||||
cmd.Env = rizinEnv()
|
||||
configureSubprocess(cmd)
|
||||
out, err := cmd.CombinedOutput()
|
||||
cancel()
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
|
||||
text := strings.TrimSpace(string(out))
|
||||
if text == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
lines := []string{}
|
||||
for _, line := range strings.Split(text, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line != "" {
|
||||
lines = append(lines, line)
|
||||
}
|
||||
}
|
||||
if len(lines) > 0 {
|
||||
return lines, nil
|
||||
}
|
||||
}
|
||||
|
||||
if lastErr == nil {
|
||||
lastErr = errors.New("no version output")
|
||||
}
|
||||
return nil, lastErr
|
||||
}
|
||||
|
||||
// rizinEnv returns the current environment with RIZIN_NOTEBOOK_NO_AUTOSTART=1
|
||||
// so the rz_notebook plugin in sub-processes won't try to auto-start a server.
|
||||
func rizinEnv() []string {
|
||||
env := os.Environ()
|
||||
// Ensure the variable is present (even if main.go already set it).
|
||||
found := false
|
||||
for i, e := range env {
|
||||
if strings.HasPrefix(e, "RIZIN_NOTEBOOK_NO_AUTOSTART=") {
|
||||
env[i] = "RIZIN_NOTEBOOK_NO_AUTOSTART=1"
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
env = append(env, "RIZIN_NOTEBOOK_NO_AUTOSTART=1")
|
||||
}
|
||||
return env
|
||||
}
|
||||
|
||||
func RizinCommands(rizinbin string) (map[string]RizinCommand, error) {
|
||||
cmd := exec.Command(rizinbin, "-qc", "?*j")
|
||||
cmd.Env = rizinEnv()
|
||||
configureSubprocess(cmd)
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return strings.Split(string(out), "\n"), nil
|
||||
|
||||
var commands map[string]RizinCommand
|
||||
err = json.Unmarshal(out, &commands)
|
||||
return commands, err
|
||||
}
|
||||
|
||||
// Launches Rizin with -2 -0 -e scr.color=3 -p <project> and waits for initial null-byte.
|
||||
func NewRizin(rizinbin, file, project string) *Rizin {
|
||||
args := []string{
|
||||
"-2",
|
||||
"-0",
|
||||
"-e",
|
||||
"scr.color=3",
|
||||
"-p",
|
||||
project,
|
||||
"-e", "scr.color=3",
|
||||
"-p", project,
|
||||
file,
|
||||
}
|
||||
pipe := exec.Command(rizinbin, args...)
|
||||
pipe.Env = rizinEnv()
|
||||
configureSubprocess(pipe)
|
||||
|
||||
stdin, err := pipe.StdinPipe()
|
||||
if err != nil {
|
||||
@@ -54,34 +159,99 @@ func NewRizin(rizinbin, file, project string) *Rizin {
|
||||
return nil
|
||||
}
|
||||
|
||||
rizin := &Rizin{pipe: pipe, stdin: stdin, stdout: stdout, project: project}
|
||||
rizin := &Rizin{
|
||||
pipe: pipe,
|
||||
stdin: stdin,
|
||||
reader: bufio.NewReader(stdout),
|
||||
project: project,
|
||||
closed: false,
|
||||
}
|
||||
|
||||
// Wait for the initial null-byte prompt indicating Rizin is ready.
|
||||
// This is done in a goroutine with the mutex held to prevent commands
|
||||
// from being sent before Rizin has finished initialization.
|
||||
go func(r *Rizin) {
|
||||
r.mutex.Lock()
|
||||
if _, err := bufio.NewReader(r.stdout).ReadString('\x00'); err != nil {
|
||||
fmt.Println("pipe error:", err)
|
||||
defer r.mutex.Unlock()
|
||||
if _, err := r.reader.ReadString('\x00'); err != nil {
|
||||
fmt.Println("pipe error: initial prompt:", err)
|
||||
}
|
||||
r.mutex.Unlock()
|
||||
}(rizin)
|
||||
|
||||
return rizin
|
||||
}
|
||||
|
||||
func (r *Rizin) exec(cmd string) (string, error) {
|
||||
r.mutex.Lock()
|
||||
defer r.mutex.Unlock()
|
||||
|
||||
if r.closed {
|
||||
return "", fmt.Errorf("pipe is closed")
|
||||
}
|
||||
|
||||
if _, err := fmt.Fprintln(r.stdin, cmd); err != nil {
|
||||
fmt.Println("pipe error:", err)
|
||||
return "", err
|
||||
}
|
||||
buf, err := bufio.NewReader(r.stdout).ReadString('\x00')
|
||||
if err != nil && err != io.EOF {
|
||||
fmt.Println("pipe error:", err)
|
||||
return "", err
|
||||
|
||||
// Use a context with timeout to prevent indefinite blocking.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), pipeReadTimeout)
|
||||
defer cancel()
|
||||
|
||||
type readResult struct {
|
||||
data string
|
||||
err error
|
||||
}
|
||||
ch := make(chan readResult, 1)
|
||||
|
||||
go func() {
|
||||
buf, err := r.reader.ReadString('\x00')
|
||||
if err != nil && err != io.EOF {
|
||||
ch <- readResult{"", err}
|
||||
return
|
||||
}
|
||||
buf = string(bytes.Trim([]byte(buf), "\x00"))
|
||||
ch <- readResult{buf, nil}
|
||||
}()
|
||||
|
||||
select {
|
||||
case result := <-ch:
|
||||
if result.err != nil {
|
||||
fmt.Println("pipe error:", result.err)
|
||||
}
|
||||
return result.data, result.err
|
||||
case <-ctx.Done():
|
||||
return "", fmt.Errorf("pipe read timed out after %v", pipeReadTimeout)
|
||||
}
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
func (r *Rizin) close() {
|
||||
r.exec("Ps " + r.project)
|
||||
r.exec("q!")
|
||||
if r.closed {
|
||||
return
|
||||
}
|
||||
// Save the project before quitting. Errors are logged but not fatal.
|
||||
if _, err := r.exec("Ps " + r.project); err != nil {
|
||||
fmt.Println("pipe warning: failed to save project:", err)
|
||||
}
|
||||
|
||||
// Send quit command. Use direct stdin write since exec() checks closed flag.
|
||||
r.mutex.Lock()
|
||||
r.closed = true
|
||||
fmt.Fprintln(r.stdin, "q!")
|
||||
r.mutex.Unlock()
|
||||
|
||||
// Wait for the process to exit (with a timeout).
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- r.pipe.Wait()
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
// Process exited cleanly.
|
||||
case <-time.After(10 * time.Second):
|
||||
// Force kill if it doesn't exit in time.
|
||||
fmt.Println("pipe warning: force killing unresponsive rizin process")
|
||||
r.pipe.Process.Kill()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
//go:build !windows
|
||||
|
||||
package main
|
||||
|
||||
import "os/exec"
|
||||
|
||||
// configureSubprocess is a no-op on non-Windows platforms.
|
||||
func configureSubprocess(cmd *exec.Cmd) {}
|
||||
@@ -0,0 +1,17 @@
|
||||
//go:build windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// configureSubprocess hides the console window for spawned rizin sub-processes
|
||||
// so they don't flash a CMD window on the user's desktop.
|
||||
func configureSubprocess(cmd *exec.Cmd) {
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
HideWindow: true,
|
||||
CreationFlags: 0x08000000, // CREATE_NO_WINDOW
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
// Protocol Buffers schema for rizin-notebook API.
|
||||
// All API endpoints accept and return protobuf-encoded messages.
|
||||
// Content-Type: application/x-protobuf
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
package notebook;
|
||||
|
||||
option go_package = "github.com/rizinorg/rizin-notebook/pb";
|
||||
|
||||
// ─── Core Data Model ────────────────────────────────────────
|
||||
|
||||
// CellType enumerates the kinds of notebook cells.
|
||||
enum CellType {
|
||||
CELL_TYPE_UNSPECIFIED = 0;
|
||||
CELL_TYPE_COMMAND = 1;
|
||||
CELL_TYPE_SCRIPT = 2;
|
||||
CELL_TYPE_MARKDOWN = 3;
|
||||
}
|
||||
|
||||
// Cell represents a single notebook cell (command, script, or markdown).
|
||||
message Cell {
|
||||
string id = 1; // Unique cell identifier (8-char nonce).
|
||||
CellType type = 2; // Cell type.
|
||||
string content = 3; // Command string, script source, or markdown text.
|
||||
bytes output = 4; // Raw execution output (ANSI bytes for cmd/script, rendered HTML for markdown).
|
||||
int64 created = 5; // Unix timestamp of creation.
|
||||
int64 executed = 6; // Unix timestamp of last execution (0 if never).
|
||||
}
|
||||
|
||||
// Page represents a notebook page containing cells and associated binary.
|
||||
message Page {
|
||||
string id = 1; // Unique page identifier (32-char nonce).
|
||||
string title = 2; // Human-readable page title.
|
||||
string filename = 3; // Original filename of the uploaded binary.
|
||||
string binary = 4; // Storage key for the binary file (internal use).
|
||||
bool pipe = 5; // Whether a Rizin pipe is currently open.
|
||||
int64 created = 6; // Unix timestamp of creation.
|
||||
int64 modified = 7; // Unix timestamp of last modification.
|
||||
repeated Cell cells = 8; // Ordered list of cells.
|
||||
string binary_hash = 9; // SHA-256 hex digest of the bundled binary.
|
||||
}
|
||||
|
||||
// ─── Request / Response Messages ────────────────────────────
|
||||
|
||||
// StatusResponse is returned by the health/status endpoint.
|
||||
message StatusResponse {
|
||||
string version = 1; // Notebook server version string.
|
||||
string rizin_version = 2; // Rizin version string.
|
||||
string rizin_path = 3; // Resolved path to rizin binary.
|
||||
string storage = 4; // Notebook data directory path.
|
||||
int32 pages = 5; // Total number of pages.
|
||||
int32 open_pipes = 6; // Number of currently open Rizin pipes.
|
||||
}
|
||||
|
||||
// ListPagesResponse contains a summary of all pages.
|
||||
message ListPagesResponse {
|
||||
repeated Page pages = 1;
|
||||
}
|
||||
|
||||
// GetPageResponse returns a single page with all cells.
|
||||
message GetPageResponse {
|
||||
Page page = 1;
|
||||
}
|
||||
|
||||
// CreatePageRequest creates a new notebook page.
|
||||
message CreatePageRequest {
|
||||
string title = 1; // Required: page title.
|
||||
string filename = 2; // Required: original binary filename.
|
||||
bytes binary = 3; // Required: binary file content.
|
||||
}
|
||||
|
||||
// CreatePageResponse returns the created page.
|
||||
message CreatePageResponse {
|
||||
Page page = 1;
|
||||
}
|
||||
|
||||
// UpdatePageRequest modifies page metadata.
|
||||
message UpdatePageRequest {
|
||||
string id = 1; // Required: page ID.
|
||||
string title = 2; // New title (if non-empty).
|
||||
}
|
||||
|
||||
// DeletePageRequest removes a page and all its cells.
|
||||
message DeletePageRequest {
|
||||
string id = 1; // Required: page ID.
|
||||
}
|
||||
|
||||
// ─── Cell Operations ────────────────────────────────────────
|
||||
|
||||
// AddCellRequest adds a new cell to a page.
|
||||
message AddCellRequest {
|
||||
string page_id = 1; // Required: parent page ID.
|
||||
CellType type = 2; // Required: cell type.
|
||||
string content = 3; // Command, script source, or initial markdown.
|
||||
}
|
||||
|
||||
// AddCellResponse returns the created cell.
|
||||
message AddCellResponse {
|
||||
Cell cell = 1;
|
||||
}
|
||||
|
||||
// UpdateCellRequest modifies a cell's content.
|
||||
message UpdateCellRequest {
|
||||
string page_id = 1; // Required: parent page ID.
|
||||
string cell_id = 2; // Required: cell ID.
|
||||
string content = 3; // New content (command, script, or markdown).
|
||||
}
|
||||
|
||||
// DeleteCellRequest removes a cell from a page.
|
||||
message DeleteCellRequest {
|
||||
string page_id = 1; // Required: parent page ID.
|
||||
string cell_id = 2; // Required: cell ID.
|
||||
}
|
||||
|
||||
// GetCellOutputRequest retrieves the output of a cell.
|
||||
message GetCellOutputRequest {
|
||||
string page_id = 1;
|
||||
string cell_id = 2;
|
||||
}
|
||||
|
||||
// GetCellOutputResponse returns raw output bytes.
|
||||
message GetCellOutputResponse {
|
||||
bytes output = 1; // Raw ANSI output bytes.
|
||||
string html = 2; // Pre-rendered HTML (ANSI-to-HTML converted).
|
||||
int64 executed_at = 3; // Unix timestamp of execution.
|
||||
}
|
||||
|
||||
// ─── Execution ──────────────────────────────────────────────
|
||||
|
||||
// ExecCommandRequest executes a rizin command and stores result as a cell.
|
||||
message ExecCommandRequest {
|
||||
string page_id = 1; // Required: page ID (must have open pipe).
|
||||
string command = 2; // Required: rizin command string.
|
||||
}
|
||||
|
||||
// ExecCommandResponse returns execution result.
|
||||
message ExecCommandResponse {
|
||||
Cell cell = 1; // The created command cell with output.
|
||||
bool success = 2; // Whether execution succeeded.
|
||||
string error = 3; // Error message if failed.
|
||||
}
|
||||
|
||||
// ExecScriptRequest executes a JavaScript script.
|
||||
message ExecScriptRequest {
|
||||
string page_id = 1; // Required: page ID.
|
||||
string script = 2; // Required: JavaScript source code.
|
||||
}
|
||||
|
||||
// ExecScriptResponse returns script execution result.
|
||||
message ExecScriptResponse {
|
||||
Cell cell = 1; // The created script cell with output.
|
||||
bool success = 2;
|
||||
string error = 3;
|
||||
}
|
||||
|
||||
// ─── Record (client-side execution) ─────────────────────────
|
||||
|
||||
// RecordCommandRequest records a pre-executed command+output as a cell.
|
||||
// Used by clients that execute commands locally (rizin plugin, Cutter).
|
||||
message RecordCommandRequest {
|
||||
string page_id = 1; // Required: page ID.
|
||||
string command = 2; // Required: the command that was executed.
|
||||
bytes output = 3; // Required: raw execution output.
|
||||
}
|
||||
|
||||
// RecordCommandResponse returns the recorded cell.
|
||||
message RecordCommandResponse {
|
||||
Cell cell = 1; // The created command cell.
|
||||
bool success = 2;
|
||||
string error = 3;
|
||||
}
|
||||
|
||||
// ─── Pipe Management ────────────────────────────────────────
|
||||
|
||||
// PipeRequest opens or closes a Rizin pipe for a page.
|
||||
message PipeRequest {
|
||||
string page_id = 1; // Required: page ID.
|
||||
}
|
||||
|
||||
// PipeResponse reports pipe status.
|
||||
message PipeResponse {
|
||||
bool open = 1; // Whether the pipe is now open.
|
||||
string error = 2; // Error message if operation failed.
|
||||
}
|
||||
|
||||
// ─── Import / Export ────────────────────────────────────────
|
||||
|
||||
// ExportPageResponse returns the raw .rznb database file for a page.
|
||||
message ExportPageResponse {
|
||||
bytes data = 1; // The complete .rznb SQLite database file.
|
||||
string filename = 2; // Suggested filename (e.g. "page-title.rznb").
|
||||
}
|
||||
|
||||
// ImportPageResponse returns the imported page.
|
||||
message ImportPageResponse {
|
||||
Page page = 1; // The imported page.
|
||||
}
|
||||
|
||||
// DownloadBinaryResponse returns the raw binary associated with a page.
|
||||
message DownloadBinaryResponse {
|
||||
bytes data = 1; // The raw binary file content.
|
||||
string filename = 2; // Original filename (e.g. "crackme.exe").
|
||||
string hash = 3; // SHA-256 hex digest of the binary.
|
||||
}
|
||||
|
||||
// ─── Settings ───────────────────────────────────────────────
|
||||
|
||||
// SettingsResponse returns all environment variable settings.
|
||||
message SettingsResponse {
|
||||
map<string, string> environment = 1;
|
||||
}
|
||||
|
||||
// SetSettingRequest sets an environment variable.
|
||||
message SetSettingRequest {
|
||||
string key = 1;
|
||||
string value = 2;
|
||||
}
|
||||
|
||||
// DeleteSettingRequest removes an environment variable.
|
||||
message DeleteSettingRequest {
|
||||
string key = 1;
|
||||
}
|
||||
|
||||
// ─── Generic ────────────────────────────────────────────────
|
||||
|
||||
// ErrorResponse is returned on any API error.
|
||||
message ErrorResponse {
|
||||
int32 code = 1; // HTTP status code.
|
||||
string message = 2; // Human-readable error description.
|
||||
}
|
||||
|
||||
// SuccessResponse is a generic acknowledgement.
|
||||
message SuccessResponse {
|
||||
bool ok = 1;
|
||||
string message = 2;
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(rizin-notebook-plugin C)
|
||||
|
||||
set(CMAKE_C_STANDARD 11)
|
||||
set(CMAKE_C_STANDARD_REQUIRED ON)
|
||||
|
||||
# ── Find Rizin ────────────────────────────────────────────────────────────
|
||||
# Option A: Cutter SDK (provides Rizin libraries + headers).
|
||||
# cmake -S . -B build -DCUTTER_SDK_DIR="C:/path/to/Cutter-v2.4.1-Windows-x86_64"
|
||||
#
|
||||
# Option B: Build-tree mode (in-tree meson build).
|
||||
# cmake -S . -B build
|
||||
# -DRIZIN_SOURCE_DIR="C:/path/to/rizin"
|
||||
# -DRIZIN_BUILD_DIR="C:/path/to/rizin/build"
|
||||
#
|
||||
# Option C: Installed rizin — find_package(Rizin) just works.
|
||||
|
||||
set(CUTTER_SDK_DIR "" CACHE PATH "Path to Cutter SDK (contains lib/cmake/Rizin)")
|
||||
set(RIZIN_SOURCE_DIR "" CACHE PATH "Path to rizin source tree")
|
||||
set(RIZIN_BUILD_DIR "" CACHE PATH "Path to rizin build directory")
|
||||
|
||||
if(CUTTER_SDK_DIR)
|
||||
# ── Cutter SDK mode ──
|
||||
message(STATUS "Using Cutter SDK: ${CUTTER_SDK_DIR}")
|
||||
set(CMAKE_PREFIX_PATH "${CUTTER_SDK_DIR}/lib/cmake/Rizin" ${CMAKE_PREFIX_PATH})
|
||||
find_package(Rizin COMPONENTS Core REQUIRED)
|
||||
set(RZ_LIBRARIES Rizin::Core)
|
||||
get_target_property(RZ_INCLUDE_DIRS Rizin::Core INTERFACE_INCLUDE_DIRECTORIES)
|
||||
elseif(RIZIN_SOURCE_DIR AND RIZIN_BUILD_DIR)
|
||||
# ── Build-tree mode: use source/build directories directly ──
|
||||
message(STATUS "Using rizin build tree:")
|
||||
message(STATUS " Source: ${RIZIN_SOURCE_DIR}")
|
||||
message(STATUS " Build: ${RIZIN_BUILD_DIR}")
|
||||
|
||||
set(RZ_INCLUDE_DIRS
|
||||
"${RIZIN_SOURCE_DIR}/librz/include"
|
||||
"${RIZIN_SOURCE_DIR}/librz/util/sdb/src"
|
||||
"${RIZIN_SOURCE_DIR}/subprojects/rzheap"
|
||||
"${RIZIN_BUILD_DIR}"
|
||||
"${RIZIN_BUILD_DIR}/librz"
|
||||
)
|
||||
|
||||
set(RZ_LIBRARY_DIRS
|
||||
"${RIZIN_BUILD_DIR}/librz/core"
|
||||
"${RIZIN_BUILD_DIR}/librz/util"
|
||||
"${RIZIN_BUILD_DIR}/librz/config"
|
||||
"${RIZIN_BUILD_DIR}/librz/cons"
|
||||
"${RIZIN_BUILD_DIR}/librz/io"
|
||||
"${RIZIN_BUILD_DIR}/librz/cmd"
|
||||
)
|
||||
|
||||
find_library(RZ_CORE_LIB NAMES rz_core HINTS "${RIZIN_BUILD_DIR}/librz/core" REQUIRED)
|
||||
find_library(RZ_UTIL_LIB NAMES rz_util HINTS "${RIZIN_BUILD_DIR}/librz/util" REQUIRED)
|
||||
find_library(RZ_CONS_LIB NAMES rz_cons HINTS "${RIZIN_BUILD_DIR}/librz/cons" REQUIRED)
|
||||
find_library(RZ_CONFIG_LIB NAMES rz_config HINTS "${RIZIN_BUILD_DIR}/librz/config" REQUIRED)
|
||||
find_library(RZ_IO_LIB NAMES rz_io HINTS "${RIZIN_BUILD_DIR}/librz/io" REQUIRED)
|
||||
|
||||
set(RZ_LIBRARIES ${RZ_CORE_LIB} ${RZ_UTIL_LIB} ${RZ_CONS_LIB} ${RZ_CONFIG_LIB} ${RZ_IO_LIB})
|
||||
message(STATUS " Libraries: ${RZ_LIBRARIES}")
|
||||
else()
|
||||
# ── Installed mode ──
|
||||
find_package(Rizin COMPONENTS Core REQUIRED)
|
||||
get_target_property(RZ_INCLUDE_DIRS Rizin::Core INTERFACE_INCLUDE_DIRECTORIES)
|
||||
set(RZ_LIBRARIES Rizin::Core)
|
||||
endif()
|
||||
|
||||
# ── Find protobuf-c ──────────────────────────────────────────────────────
|
||||
# Either via CMake config (vcpkg), pkg-config, or manual path.
|
||||
find_package(protobuf-c CONFIG QUIET)
|
||||
if(protobuf-c_FOUND)
|
||||
message(STATUS "Found protobuf-c via CMake config")
|
||||
set(PROTOBUF_C_LIBS protobuf-c::protobuf-c)
|
||||
get_target_property(PROTOBUF_C_INCLUDE_DIRS protobuf-c::protobuf-c INTERFACE_INCLUDE_DIRECTORIES)
|
||||
else()
|
||||
# Fallback: manual search.
|
||||
find_library(PROTOBUF_C_LIB NAMES protobuf-c REQUIRED)
|
||||
find_path(PROTOBUF_C_INC NAMES protobuf-c/protobuf-c.h REQUIRED)
|
||||
set(PROTOBUF_C_LIBS ${PROTOBUF_C_LIB})
|
||||
set(PROTOBUF_C_INCLUDE_DIRS ${PROTOBUF_C_INC})
|
||||
message(STATUS "Found protobuf-c: ${PROTOBUF_C_LIB}")
|
||||
endif()
|
||||
|
||||
# Resolve protobuf-c runtime DLL path for Windows packaging/install.
|
||||
set(PROTOBUF_C_RUNTIME_DLL "")
|
||||
if(WIN32)
|
||||
if(TARGET protobuf-c::protobuf-c)
|
||||
get_target_property(_pb_dll_rel protobuf-c::protobuf-c IMPORTED_LOCATION_RELEASE)
|
||||
get_target_property(_pb_dll_dbg protobuf-c::protobuf-c IMPORTED_LOCATION_DEBUG)
|
||||
get_target_property(_pb_dll_any protobuf-c::protobuf-c IMPORTED_LOCATION)
|
||||
if(_pb_dll_rel)
|
||||
set(PROTOBUF_C_RUNTIME_DLL "${_pb_dll_rel}")
|
||||
elseif(_pb_dll_any)
|
||||
set(PROTOBUF_C_RUNTIME_DLL "${_pb_dll_any}")
|
||||
elseif(_pb_dll_dbg)
|
||||
set(PROTOBUF_C_RUNTIME_DLL "${_pb_dll_dbg}")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# vcpkg fallback for common setup.
|
||||
if(NOT PROTOBUF_C_RUNTIME_DLL AND EXISTS "C:/vcpkg/installed/x64-windows/bin/protobuf-c.dll")
|
||||
set(PROTOBUF_C_RUNTIME_DLL "C:/vcpkg/installed/x64-windows/bin/protobuf-c.dll")
|
||||
endif()
|
||||
|
||||
if(PROTOBUF_C_RUNTIME_DLL)
|
||||
message(STATUS "protobuf-c runtime DLL: ${PROTOBUF_C_RUNTIME_DLL}")
|
||||
else()
|
||||
message(WARNING "protobuf-c runtime DLL path not found; rz_notebook.dll may fail to load unless protobuf-c.dll is in PATH")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# ── WinHTTP (Windows) or libcurl (Unix) ──────────────────────────────────
|
||||
if(WIN32)
|
||||
set(HTTP_LIBS winhttp)
|
||||
else()
|
||||
find_package(CURL REQUIRED)
|
||||
set(HTTP_LIBS CURL::libcurl)
|
||||
endif()
|
||||
|
||||
# ── Plugin shared library ─────────────────────────────────────────────────
|
||||
add_library(rz_notebook MODULE
|
||||
src/rz_notebook.c
|
||||
src/nb_http.c
|
||||
src/nb_http.h
|
||||
src/nb_server.c
|
||||
src/nb_server.h
|
||||
src/notebook.pb-c.c
|
||||
src/notebook.pb-c.h
|
||||
)
|
||||
|
||||
target_link_libraries(rz_notebook PRIVATE
|
||||
${RZ_LIBRARIES}
|
||||
${PROTOBUF_C_LIBS}
|
||||
${HTTP_LIBS}
|
||||
)
|
||||
|
||||
target_include_directories(rz_notebook PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src
|
||||
${PROTOBUF_C_INCLUDE_DIRS}
|
||||
${RZ_INCLUDE_DIRS}
|
||||
)
|
||||
|
||||
# On Windows with shared protobuf-c (DLL), PROTOBUF_C_USE_SHARED_LIB must be
|
||||
# defined so the header provides a local static copy of protobuf_c_empty_string
|
||||
# instead of referencing an unresolved extern symbol.
|
||||
if(WIN32 AND NOT protobuf-c_FOUND)
|
||||
# Manual find — assume shared
|
||||
target_compile_definitions(rz_notebook PRIVATE PROTOBUF_C_USE_SHARED_LIB)
|
||||
elseif(WIN32)
|
||||
# vcpkg shared libs: check if DLL exists
|
||||
if(EXISTS "${_VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/bin/protobuf-c.dll"
|
||||
OR EXISTS "C:/vcpkg/installed/x64-windows/bin/protobuf-c.dll")
|
||||
target_compile_definitions(rz_notebook PRIVATE PROTOBUF_C_USE_SHARED_LIB)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Suppress warnings in generated protobuf-c code.
|
||||
if(MSVC)
|
||||
set_source_files_properties(src/notebook.pb-c.c PROPERTIES
|
||||
COMPILE_FLAGS "/wd4146 /wd4267 /wd4244")
|
||||
endif()
|
||||
|
||||
# ── Plugin install directory ─────────────────────────────────────────────
|
||||
if(WIN32)
|
||||
file(TO_CMAKE_PATH "$ENV{APPDATA}" _appdata)
|
||||
set(RZ_INSTALL_PLUGDIR "${_appdata}/rizin/plugins"
|
||||
CACHE STRING "Rizin plugin install directory")
|
||||
else()
|
||||
set(RZ_INSTALL_PLUGDIR "$ENV{HOME}/.local/lib/rizin/plugins"
|
||||
CACHE STRING "Rizin plugin install directory")
|
||||
endif()
|
||||
|
||||
install(TARGETS rz_notebook
|
||||
LIBRARY DESTINATION "${RZ_INSTALL_PLUGDIR}"
|
||||
RUNTIME DESTINATION "${RZ_INSTALL_PLUGDIR}"
|
||||
)
|
||||
|
||||
if(WIN32 AND PROTOBUF_C_RUNTIME_DLL)
|
||||
# Keep plugin self-contained at install destination.
|
||||
install(FILES "${PROTOBUF_C_RUNTIME_DLL}" DESTINATION "${RZ_INSTALL_PLUGDIR}")
|
||||
|
||||
# Also place runtime next to build output DLL for manual copy workflows and CI artifacts.
|
||||
add_custom_command(TARGET rz_notebook POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"${PROTOBUF_C_RUNTIME_DLL}"
|
||||
"$<TARGET_FILE_DIR:rz_notebook>/protobuf-c.dll"
|
||||
VERBATIM
|
||||
)
|
||||
endif()
|
||||
|
||||
message(STATUS "Rizin plugin install dir: ${RZ_INSTALL_PLUGDIR}")
|
||||
@@ -0,0 +1,133 @@
|
||||
# rizin-notebook-plugin
|
||||
|
||||
Rizin core plugin that adds `NB` commands for interacting with the
|
||||
[rizin-notebook](../README.md) server.
|
||||
|
||||
This plugin must be built against the same Rizin version as the host that loads it.
|
||||
|
||||
That means standalone rizin and Cutter may need different `rz_notebook` builds if they
|
||||
ship different Rizin versions. Both can still use the same notebook server and data.
|
||||
|
||||
## Commands
|
||||
|
||||
```
|
||||
NB list pages (default action)
|
||||
NBc <url> connect to a running notebook server
|
||||
NBs spawn (auto-start) the notebook server
|
||||
NBi show server status and session info
|
||||
NBl list all pages
|
||||
NBn [title] create a new page; auto-attaches the currently open binary
|
||||
NBo <page-id> open/select a page as the active page;
|
||||
if no binary is loaded, downloads the page binary from
|
||||
the server and opens it in rizin automatically
|
||||
NBq close/detach the active page
|
||||
NBp [page-id] show page details (active page if no id given)
|
||||
NBd <page-id> delete a page
|
||||
NBx <command> execute a command locally and record it to the active page
|
||||
NBxs <script> execute a JavaScript script on the server for the active page
|
||||
NBe [path] export the active page as a .rznb file
|
||||
NBm <path> import a .rznb file as a new page
|
||||
NBu [url] show or set the server URL
|
||||
```
|
||||
|
||||
### Typical workflow
|
||||
|
||||
```
|
||||
[rizin /path/to/binary]
|
||||
NBs -- start the server if not running
|
||||
NBn "My Analysis" -- create a page; the binary is attached automatically
|
||||
NBx aaa -- run 'aaa', output is recorded as a command cell
|
||||
NBx pdf @ main -- same
|
||||
NBe mywork.rznb -- export for sharing or backup
|
||||
```
|
||||
|
||||
### Binary auto-download
|
||||
|
||||
When you run `NBo <page-id>` and no binary is loaded in rizin, the plugin checks whether
|
||||
the page has an attached binary. If it does, it downloads the binary from the server, saves
|
||||
it to a temporary file, and opens it in rizin automatically.
|
||||
|
||||
This removes the need to locate and open the binary file manually before working on a page.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Rizin headers (from a Cutter SDK, rizin source tree, or installed rizin)
|
||||
- protobuf-c (for serializing API messages)
|
||||
- WinHTTP (Windows, built-in) or libcurl (Linux/macOS)
|
||||
- OpenSSL (Linux/macOS, for SHA-256; Windows uses WinCrypt)
|
||||
|
||||
## Build
|
||||
|
||||
### 1. Generate protobuf-c files
|
||||
|
||||
Run from the repo root. Requires `protoc` and `protoc-gen-c` on your PATH.
|
||||
|
||||
```bash
|
||||
protoc --plugin=protoc-gen-c=/path/to/protoc-gen-c \
|
||||
--c_out=rizin-notebook-plugin/src \
|
||||
proto/notebook.proto
|
||||
```
|
||||
|
||||
This generates `notebook.pb-c.c` and `notebook.pb-c.h` in
|
||||
`rizin-notebook-plugin/src/proto/`. Copy them up one directory to
|
||||
`rizin-notebook-plugin/src/` before building.
|
||||
|
||||
### 2. Configure and build
|
||||
|
||||
**Windows (Cutter SDK + vcpkg):**
|
||||
|
||||
```powershell
|
||||
cmake -S rizin-notebook-plugin -B rizin-notebook-plugin/build ^
|
||||
-DCUTTER_SDK_DIR="C:\path\to\Cutter-v2.4.1-Windows-x86_64" ^
|
||||
-DCMAKE_TOOLCHAIN_FILE="C:\vcpkg\scripts\buildsystems\vcpkg.cmake"
|
||||
|
||||
cmake --build rizin-notebook-plugin/build --config Release
|
||||
```
|
||||
|
||||
**Windows (rizin source tree + vcpkg):**
|
||||
|
||||
```powershell
|
||||
cmake -S rizin-notebook-plugin -B rizin-notebook-plugin/build ^
|
||||
-DRIZIN_SOURCE_DIR="C:\path\to\rizin" ^
|
||||
-DRIZIN_BUILD_DIR="C:\path\to\rizin\build" ^
|
||||
-DCMAKE_TOOLCHAIN_FILE="C:\vcpkg\scripts\buildsystems\vcpkg.cmake"
|
||||
|
||||
cmake --build rizin-notebook-plugin/build --config Release
|
||||
```
|
||||
|
||||
**Linux:**
|
||||
|
||||
```bash
|
||||
sudo apt install protobuf-c-compiler libprotobuf-c-dev librizin-dev \
|
||||
libcurl4-openssl-dev libssl-dev
|
||||
|
||||
cmake -S rizin-notebook-plugin -B rizin-notebook-plugin/build
|
||||
cmake --build rizin-notebook-plugin/build --config Release
|
||||
```
|
||||
|
||||
### 3. Install
|
||||
|
||||
```bash
|
||||
cmake --install rizin-notebook-plugin/build --config Release
|
||||
```
|
||||
|
||||
Or copy `rz_notebook.dll` / `rz_notebook.so` manually to the rizin plugin directory:
|
||||
|
||||
- Windows: `%APPDATA%\rizin\plugins`
|
||||
- Linux: `~/.local/lib/rizin/plugins`
|
||||
|
||||
Run `rizin -H RZ_USER_PLUGINS` to confirm the correct path.
|
||||
|
||||
If you use Cutter with a bundled Rizin, also copy the Cutter-compatible build into the
|
||||
Rizin plugin directory inside the Cutter installation.
|
||||
|
||||
## Notes
|
||||
|
||||
- The plugin does **not** auto-connect on load. You must run `NBc` or `NBs` first.
|
||||
- `NBx` runs the command in the current rizin session and records both command and output.
|
||||
It requires a binary to be open. If you used `NBo` and the binary was auto-downloaded,
|
||||
it is already open and `NBx` works immediately.
|
||||
- `NBxs` runs JavaScript on the server side via a rizin pipe. The pipe must be open
|
||||
(opened automatically by the server when you open a page with a binary).
|
||||
- `NBm` auto-selects the imported page as the active page.
|
||||
- `NBe` uses the page title as the filename if no path is given.
|
||||
@@ -0,0 +1,365 @@
|
||||
#include "nb_http.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
|
||||
// ── Configuration ───────────────────────────────────────────────────────
|
||||
|
||||
static char g_base_url[512] = "http://127.0.0.1:8000";
|
||||
|
||||
void nb_http_set_base_url(const char *base_url) {
|
||||
if (!base_url) return;
|
||||
snprintf(g_base_url, sizeof(g_base_url), "%s", base_url);
|
||||
// Strip trailing slash.
|
||||
size_t len = strlen(g_base_url);
|
||||
while (len > 0 && g_base_url[len - 1] == '/') {
|
||||
g_base_url[--len] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
const char *nb_http_get_base_url(void) {
|
||||
return g_base_url;
|
||||
}
|
||||
|
||||
void nb_http_response_free(NbHttpResponse *resp) {
|
||||
if (!resp) return;
|
||||
free(resp->body);
|
||||
free(resp->error);
|
||||
resp->body = NULL;
|
||||
resp->error = NULL;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// Windows implementation using WinHTTP
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
#ifdef _WIN32
|
||||
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <windows.h>
|
||||
#include <winhttp.h>
|
||||
#pragma comment(lib, "winhttp.lib")
|
||||
|
||||
static HINTERNET g_session = NULL;
|
||||
|
||||
bool nb_http_init(void) {
|
||||
if (g_session) return true;
|
||||
g_session = WinHttpOpen(L"rizin-notebook/1.0",
|
||||
WINHTTP_ACCESS_TYPE_NO_PROXY,
|
||||
WINHTTP_NO_PROXY_NAME,
|
||||
WINHTTP_NO_PROXY_BYPASS, 0);
|
||||
if (!g_session) return false;
|
||||
|
||||
// Set timeouts: 5s connect, 10s send/recv.
|
||||
WinHttpSetTimeouts(g_session, 5000, 5000, 10000, 10000);
|
||||
return true;
|
||||
}
|
||||
|
||||
void nb_http_fini(void) {
|
||||
if (g_session) {
|
||||
WinHttpCloseHandle(g_session);
|
||||
g_session = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
static wchar_t *build_url_wide(const char *path) {
|
||||
char full[1024];
|
||||
snprintf(full, sizeof(full), "%s%s", g_base_url, path);
|
||||
|
||||
int wlen = MultiByteToWideChar(CP_UTF8, 0, full, -1, NULL, 0);
|
||||
if (wlen <= 0) return NULL;
|
||||
wchar_t *wurl = malloc(wlen * sizeof(wchar_t));
|
||||
if (!wurl) return NULL;
|
||||
MultiByteToWideChar(CP_UTF8, 0, full, -1, wurl, wlen);
|
||||
return wurl;
|
||||
}
|
||||
|
||||
static bool winhttp_request(const char *method, const char *path,
|
||||
const uint8_t *body, size_t body_len,
|
||||
NbHttpResponse *resp) {
|
||||
memset(resp, 0, sizeof(*resp));
|
||||
|
||||
if (!g_session) {
|
||||
resp->error = _strdup("HTTP subsystem not initialized");
|
||||
return false;
|
||||
}
|
||||
|
||||
wchar_t *url_w = build_url_wide(path);
|
||||
if (!url_w) {
|
||||
resp->error = _strdup("Failed to build URL");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Crack URL.
|
||||
URL_COMPONENTS uc;
|
||||
memset(&uc, 0, sizeof(uc));
|
||||
uc.dwStructSize = sizeof(uc);
|
||||
uc.dwSchemeLength = (DWORD)-1;
|
||||
uc.dwHostNameLength = (DWORD)-1;
|
||||
uc.dwUrlPathLength = (DWORD)-1;
|
||||
uc.dwExtraInfoLength = (DWORD)-1;
|
||||
|
||||
if (!WinHttpCrackUrl(url_w, 0, 0, &uc)) {
|
||||
resp->error = _strdup("Failed to parse URL");
|
||||
free(url_w);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Extract host.
|
||||
wchar_t host[256] = {0};
|
||||
if (uc.dwHostNameLength > 0 && uc.dwHostNameLength < 255) {
|
||||
wcsncpy(host, uc.lpszHostName, uc.dwHostNameLength);
|
||||
host[uc.dwHostNameLength] = L'\0';
|
||||
}
|
||||
|
||||
// Extract path (includes query string).
|
||||
wchar_t req_path[512] = {0};
|
||||
if (uc.dwUrlPathLength > 0) {
|
||||
size_t plen = uc.dwUrlPathLength + uc.dwExtraInfoLength;
|
||||
if (plen < 511) {
|
||||
wcsncpy(req_path, uc.lpszUrlPath, plen);
|
||||
req_path[plen] = L'\0';
|
||||
}
|
||||
}
|
||||
|
||||
// Convert method to wide.
|
||||
wchar_t method_w[16] = {0};
|
||||
MultiByteToWideChar(CP_UTF8, 0, method, -1, method_w, 16);
|
||||
|
||||
HINTERNET conn = WinHttpConnect(g_session, host, uc.nPort, 0);
|
||||
if (!conn) {
|
||||
resp->error = _strdup("WinHttpConnect failed");
|
||||
free(url_w);
|
||||
return false;
|
||||
}
|
||||
|
||||
HINTERNET req = WinHttpOpenRequest(conn, method_w, req_path,
|
||||
NULL, WINHTTP_NO_REFERER,
|
||||
WINHTTP_DEFAULT_ACCEPT_TYPES, 0);
|
||||
if (!req) {
|
||||
resp->error = _strdup("WinHttpOpenRequest failed");
|
||||
WinHttpCloseHandle(conn);
|
||||
free(url_w);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Set content type header for protobuf.
|
||||
if (body && body_len > 0) {
|
||||
WinHttpAddRequestHeaders(req,
|
||||
L"Content-Type: application/x-protobuf\r\n",
|
||||
(DWORD)-1, WINHTTP_ADDREQ_FLAG_ADD | WINHTTP_ADDREQ_FLAG_REPLACE);
|
||||
}
|
||||
WinHttpAddRequestHeaders(req,
|
||||
L"Accept: application/x-protobuf\r\n",
|
||||
(DWORD)-1, WINHTTP_ADDREQ_FLAG_ADD | WINHTTP_ADDREQ_FLAG_REPLACE);
|
||||
|
||||
// Send.
|
||||
BOOL ok = WinHttpSendRequest(req,
|
||||
WINHTTP_NO_ADDITIONAL_HEADERS, 0,
|
||||
(LPVOID)body, (DWORD)body_len,
|
||||
(DWORD)body_len, 0);
|
||||
if (!ok) {
|
||||
DWORD err = GetLastError();
|
||||
char msg[128];
|
||||
snprintf(msg, sizeof(msg), "WinHttpSendRequest failed: %lu", err);
|
||||
resp->error = _strdup(msg);
|
||||
WinHttpCloseHandle(req);
|
||||
WinHttpCloseHandle(conn);
|
||||
free(url_w);
|
||||
return false;
|
||||
}
|
||||
|
||||
ok = WinHttpReceiveResponse(req, NULL);
|
||||
if (!ok) {
|
||||
DWORD err = GetLastError();
|
||||
char msg[128];
|
||||
snprintf(msg, sizeof(msg), "WinHttpReceiveResponse failed: %lu", err);
|
||||
resp->error = _strdup(msg);
|
||||
WinHttpCloseHandle(req);
|
||||
WinHttpCloseHandle(conn);
|
||||
free(url_w);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get status code.
|
||||
DWORD status_code = 0;
|
||||
DWORD sz = sizeof(status_code);
|
||||
WinHttpQueryHeaders(req, WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER,
|
||||
WINHTTP_HEADER_NAME_BY_INDEX, &status_code, &sz,
|
||||
WINHTTP_NO_HEADER_INDEX);
|
||||
resp->status = (int)status_code;
|
||||
|
||||
// Read body.
|
||||
size_t total = 0;
|
||||
size_t cap = 4096;
|
||||
uint8_t *buf = malloc(cap);
|
||||
if (!buf) {
|
||||
resp->error = _strdup("Out of memory");
|
||||
WinHttpCloseHandle(req);
|
||||
WinHttpCloseHandle(conn);
|
||||
free(url_w);
|
||||
return false;
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
DWORD available = 0;
|
||||
if (!WinHttpQueryDataAvailable(req, &available) || available == 0) break;
|
||||
|
||||
if (total + available > cap) {
|
||||
while (cap < total + available) cap *= 2;
|
||||
uint8_t *nb = realloc(buf, cap);
|
||||
if (!nb) break;
|
||||
buf = nb;
|
||||
}
|
||||
|
||||
DWORD read = 0;
|
||||
if (!WinHttpReadData(req, buf + total, available, &read)) break;
|
||||
total += read;
|
||||
}
|
||||
|
||||
resp->body = buf;
|
||||
resp->body_len = total;
|
||||
resp->ok = true;
|
||||
|
||||
WinHttpCloseHandle(req);
|
||||
WinHttpCloseHandle(conn);
|
||||
free(url_w);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool nb_http_get(const char *path, NbHttpResponse *resp) {
|
||||
return winhttp_request("GET", path, NULL, 0, resp);
|
||||
}
|
||||
|
||||
bool nb_http_post(const char *path, const uint8_t *body, size_t body_len,
|
||||
NbHttpResponse *resp) {
|
||||
return winhttp_request("POST", path, body, body_len, resp);
|
||||
}
|
||||
|
||||
bool nb_http_put(const char *path, const uint8_t *body, size_t body_len,
|
||||
NbHttpResponse *resp) {
|
||||
return winhttp_request("PUT", path, body, body_len, resp);
|
||||
}
|
||||
|
||||
bool nb_http_delete(const char *path, NbHttpResponse *resp) {
|
||||
return winhttp_request("DELETE", path, NULL, 0, resp);
|
||||
}
|
||||
|
||||
#else
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// Non-Windows implementation using libcurl
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
#include <curl/curl.h>
|
||||
|
||||
static CURL *g_curl = NULL;
|
||||
|
||||
bool nb_http_init(void) {
|
||||
curl_global_init(CURL_GLOBAL_DEFAULT);
|
||||
g_curl = curl_easy_init();
|
||||
return g_curl != NULL;
|
||||
}
|
||||
|
||||
void nb_http_fini(void) {
|
||||
if (g_curl) {
|
||||
curl_easy_cleanup(g_curl);
|
||||
g_curl = NULL;
|
||||
}
|
||||
curl_global_cleanup();
|
||||
}
|
||||
|
||||
struct curl_buf {
|
||||
uint8_t *data;
|
||||
size_t len;
|
||||
size_t cap;
|
||||
};
|
||||
|
||||
static size_t curl_write_cb(void *ptr, size_t size, size_t nmemb, void *userdata) {
|
||||
struct curl_buf *cb = (struct curl_buf *)userdata;
|
||||
size_t total = size * nmemb;
|
||||
if (cb->len + total > cb->cap) {
|
||||
while (cb->cap < cb->len + total) cb->cap *= 2;
|
||||
uint8_t *nb = realloc(cb->data, cb->cap);
|
||||
if (!nb) return 0;
|
||||
cb->data = nb;
|
||||
}
|
||||
memcpy(cb->data + cb->len, ptr, total);
|
||||
cb->len += total;
|
||||
return total;
|
||||
}
|
||||
|
||||
static bool curl_request(const char *method, const char *path,
|
||||
const uint8_t *body, size_t body_len,
|
||||
NbHttpResponse *resp) {
|
||||
memset(resp, 0, sizeof(*resp));
|
||||
if (!g_curl) {
|
||||
resp->error = strdup("HTTP subsystem not initialized");
|
||||
return false;
|
||||
}
|
||||
|
||||
char url[1024];
|
||||
snprintf(url, sizeof(url), "%s%s", g_base_url, path);
|
||||
|
||||
CURL *c = g_curl;
|
||||
curl_easy_reset(c);
|
||||
|
||||
struct curl_buf cb = { .data = malloc(4096), .len = 0, .cap = 4096 };
|
||||
if (!cb.data) {
|
||||
resp->error = strdup("Out of memory");
|
||||
return false;
|
||||
}
|
||||
|
||||
curl_easy_setopt(c, CURLOPT_URL, url);
|
||||
curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, curl_write_cb);
|
||||
curl_easy_setopt(c, CURLOPT_WRITEDATA, &cb);
|
||||
curl_easy_setopt(c, CURLOPT_CONNECTTIMEOUT, 5L);
|
||||
curl_easy_setopt(c, CURLOPT_TIMEOUT, 10L);
|
||||
curl_easy_setopt(c, CURLOPT_CUSTOMREQUEST, method);
|
||||
|
||||
struct curl_slist *headers = NULL;
|
||||
headers = curl_slist_append(headers, "Accept: application/x-protobuf");
|
||||
|
||||
if (body && body_len > 0) {
|
||||
headers = curl_slist_append(headers, "Content-Type: application/x-protobuf");
|
||||
curl_easy_setopt(c, CURLOPT_POSTFIELDS, body);
|
||||
curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, (long)body_len);
|
||||
}
|
||||
curl_easy_setopt(c, CURLOPT_HTTPHEADER, headers);
|
||||
|
||||
CURLcode res = curl_easy_perform(c);
|
||||
curl_slist_free_all(headers);
|
||||
|
||||
if (res != CURLE_OK) {
|
||||
resp->error = strdup(curl_easy_strerror(res));
|
||||
free(cb.data);
|
||||
return false;
|
||||
}
|
||||
|
||||
long code = 0;
|
||||
curl_easy_getinfo(c, CURLINFO_RESPONSE_CODE, &code);
|
||||
resp->status = (int)code;
|
||||
resp->body = cb.data;
|
||||
resp->body_len = cb.len;
|
||||
resp->ok = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool nb_http_get(const char *path, NbHttpResponse *resp) {
|
||||
return curl_request("GET", path, NULL, 0, resp);
|
||||
}
|
||||
|
||||
bool nb_http_post(const char *path, const uint8_t *body, size_t body_len,
|
||||
NbHttpResponse *resp) {
|
||||
return curl_request("POST", path, body, body_len, resp);
|
||||
}
|
||||
|
||||
bool nb_http_put(const char *path, const uint8_t *body, size_t body_len,
|
||||
NbHttpResponse *resp) {
|
||||
return curl_request("PUT", path, body, body_len, resp);
|
||||
}
|
||||
|
||||
bool nb_http_delete(const char *path, NbHttpResponse *resp) {
|
||||
return curl_request("DELETE", path, NULL, 0, resp);
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,47 @@
|
||||
// HTTP client for communicating with the notebook server.
|
||||
// Uses WinHTTP on Windows, libcurl on other platforms.
|
||||
|
||||
#ifndef NB_HTTP_H
|
||||
#define NB_HTTP_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct {
|
||||
int status;
|
||||
uint8_t *body;
|
||||
size_t body_len;
|
||||
bool ok;
|
||||
char *error;
|
||||
} NbHttpResponse;
|
||||
|
||||
void nb_http_response_free(NbHttpResponse *resp);
|
||||
|
||||
bool nb_http_init(void);
|
||||
|
||||
void nb_http_fini(void);
|
||||
|
||||
void nb_http_set_base_url(const char *base_url);
|
||||
|
||||
const char *nb_http_get_base_url(void);
|
||||
|
||||
bool nb_http_get(const char *path, NbHttpResponse *resp);
|
||||
|
||||
bool nb_http_post(const char *path, const uint8_t *body, size_t body_len,
|
||||
NbHttpResponse *resp);
|
||||
|
||||
bool nb_http_put(const char *path, const uint8_t *body, size_t body_len,
|
||||
NbHttpResponse *resp);
|
||||
|
||||
bool nb_http_delete(const char *path, NbHttpResponse *resp);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif // NB_HTTP_H
|
||||
@@ -0,0 +1,264 @@
|
||||
#include "nb_server.h"
|
||||
#include "nb_http.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <windows.h>
|
||||
#include <shlobj.h>
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#include <limits.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/wait.h>
|
||||
#include <errno.h>
|
||||
#endif
|
||||
|
||||
#ifndef _WIN32
|
||||
#ifndef PATH_MAX
|
||||
#define PATH_MAX 4096
|
||||
#endif
|
||||
#endif
|
||||
|
||||
// ── Health Check ────────────────────────────────────────────────────────
|
||||
|
||||
bool nb_server_is_alive(void) {
|
||||
NbHttpResponse resp;
|
||||
bool ok = nb_http_get("/api/v1/ping", &resp);
|
||||
bool alive = ok && resp.ok && (resp.status == 200 || resp.status == 204);
|
||||
nb_http_response_free(&resp);
|
||||
return alive;
|
||||
}
|
||||
|
||||
// ── Executable Discovery ────────────────────────────────────────────────
|
||||
|
||||
#ifdef _WIN32
|
||||
static const char *NB_EXE_NAME = "rizin-notebook.exe";
|
||||
#else
|
||||
static const char *NB_EXE_NAME = "rizin-notebook";
|
||||
#endif
|
||||
|
||||
static bool file_exists(const char *path) {
|
||||
#ifdef _WIN32
|
||||
DWORD attr = GetFileAttributesA(path);
|
||||
return attr != INVALID_FILE_ATTRIBUTES && !(attr & FILE_ATTRIBUTE_DIRECTORY);
|
||||
#else
|
||||
struct stat st;
|
||||
return stat(path, &st) == 0 && S_ISREG(st.st_mode);
|
||||
#endif
|
||||
}
|
||||
|
||||
char *nb_server_find_executable(void) {
|
||||
#ifdef _WIN32
|
||||
char buf[MAX_PATH + 1];
|
||||
#else
|
||||
char buf[PATH_MAX + 1];
|
||||
#endif
|
||||
|
||||
#ifdef _WIN32
|
||||
// 1. Next to the plugin DLL (same directory as rz_notebook.dll).
|
||||
{
|
||||
HMODULE hMod = NULL;
|
||||
// Try to get our own module handle.
|
||||
if (GetModuleHandleExA(
|
||||
GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
|
||||
GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
|
||||
(LPCSTR)nb_server_find_executable, &hMod)) {
|
||||
if (GetModuleFileNameA(hMod, buf, MAX_PATH)) {
|
||||
char *sep = strrchr(buf, '\\');
|
||||
if (sep) {
|
||||
sep[1] = '\0';
|
||||
strncat(buf, NB_EXE_NAME, MAX_PATH - strlen(buf));
|
||||
if (file_exists(buf)) return _strdup(buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. In PATH.
|
||||
{
|
||||
DWORD r = SearchPathA(NULL, NB_EXE_NAME, NULL, MAX_PATH, buf, NULL);
|
||||
if (r > 0 && r < MAX_PATH && file_exists(buf)) return _strdup(buf);
|
||||
}
|
||||
|
||||
// 3. Common locations.
|
||||
{
|
||||
const char *appdata = getenv("LOCALAPPDATA");
|
||||
if (appdata) {
|
||||
snprintf(buf, sizeof(buf), "%s\\rizin-notebook\\%s", appdata, NB_EXE_NAME);
|
||||
if (file_exists(buf)) return _strdup(buf);
|
||||
}
|
||||
}
|
||||
{
|
||||
const char *progfiles = getenv("PROGRAMFILES");
|
||||
if (progfiles) {
|
||||
snprintf(buf, sizeof(buf), "%s\\rizin-notebook\\%s", progfiles, NB_EXE_NAME);
|
||||
if (file_exists(buf)) return _strdup(buf);
|
||||
}
|
||||
}
|
||||
|
||||
#else // POSIX
|
||||
|
||||
// 1. In PATH (use which-like logic).
|
||||
{
|
||||
const char *path_env = getenv("PATH");
|
||||
if (path_env) {
|
||||
char *path_copy = strdup(path_env);
|
||||
char *saveptr = NULL;
|
||||
char *dir = strtok_r(path_copy, ":", &saveptr);
|
||||
while (dir) {
|
||||
snprintf(buf, sizeof(buf), "%s/%s", dir, NB_EXE_NAME);
|
||||
if (file_exists(buf)) {
|
||||
char *result = strdup(buf);
|
||||
free(path_copy);
|
||||
return result;
|
||||
}
|
||||
dir = strtok_r(NULL, ":", &saveptr);
|
||||
}
|
||||
free(path_copy);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Common locations.
|
||||
{
|
||||
const char *home = getenv("HOME");
|
||||
if (home) {
|
||||
snprintf(buf, sizeof(buf), "%s/.local/bin/%s", home, NB_EXE_NAME);
|
||||
if (file_exists(buf)) return strdup(buf);
|
||||
snprintf(buf, sizeof(buf), "%s/go/bin/%s", home, NB_EXE_NAME);
|
||||
if (file_exists(buf)) return strdup(buf);
|
||||
}
|
||||
}
|
||||
{
|
||||
snprintf(buf, sizeof(buf), "/usr/local/bin/%s", NB_EXE_NAME);
|
||||
if (file_exists(buf)) return strdup(buf);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// ── Server Start ────────────────────────────────────────────────────────
|
||||
|
||||
#ifdef _WIN32
|
||||
|
||||
static HANDLE g_server_process = NULL;
|
||||
|
||||
static bool start_server_process(const char *exe_path) {
|
||||
STARTUPINFOA si;
|
||||
PROCESS_INFORMATION pi;
|
||||
memset(&si, 0, sizeof(si));
|
||||
si.cb = sizeof(si);
|
||||
si.dwFlags = STARTF_USESHOWWINDOW;
|
||||
si.wShowWindow = SW_HIDE;
|
||||
memset(&pi, 0, sizeof(pi));
|
||||
|
||||
char cmdline[MAX_PATH + 32];
|
||||
snprintf(cmdline, sizeof(cmdline), "\"%s\"", exe_path);
|
||||
|
||||
if (!CreateProcessA(NULL, cmdline, NULL, NULL, FALSE,
|
||||
CREATE_NO_WINDOW | DETACHED_PROCESS,
|
||||
NULL, NULL, &si, &pi)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
g_server_process = pi.hProcess;
|
||||
CloseHandle(pi.hThread);
|
||||
return true;
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
static pid_t g_server_pid = 0;
|
||||
|
||||
static bool start_server_process(const char *exe_path) {
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) return false;
|
||||
if (pid == 0) {
|
||||
// Child process: become daemon.
|
||||
setsid();
|
||||
// Redirect stdout/stderr to /dev/null.
|
||||
FILE *out = freopen("/dev/null", "w", stdout);
|
||||
FILE *err = freopen("/dev/null", "w", stderr);
|
||||
if (!out || !err) {
|
||||
_exit(127);
|
||||
}
|
||||
execl(exe_path, exe_path, (char *)NULL);
|
||||
_exit(127);
|
||||
}
|
||||
g_server_pid = pid;
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
bool nb_server_wait_alive(int timeout_ms) {
|
||||
int elapsed = 0;
|
||||
int interval = 200; // ms
|
||||
|
||||
while (elapsed < timeout_ms) {
|
||||
if (nb_server_is_alive()) return true;
|
||||
#ifdef _WIN32
|
||||
Sleep(interval);
|
||||
#else
|
||||
usleep(interval * 1000);
|
||||
#endif
|
||||
elapsed += interval;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool nb_server_ensure(const char *exe_path) {
|
||||
// Already running?
|
||||
if (nb_server_is_alive()) return true;
|
||||
|
||||
// Find executable.
|
||||
char *found = NULL;
|
||||
if (exe_path && file_exists(exe_path)) {
|
||||
found = NULL; // Use the provided path directly.
|
||||
} else {
|
||||
found = nb_server_find_executable();
|
||||
if (!found) {
|
||||
fprintf(stderr, "[notebook] cannot find rizin-notebook executable "
|
||||
"(searched next to plugin DLL, PATH, common locations)\n");
|
||||
return false;
|
||||
}
|
||||
exe_path = found;
|
||||
}
|
||||
|
||||
fprintf(stderr, "[notebook] starting server: %s\n", exe_path);
|
||||
|
||||
// Start it.
|
||||
bool ok = start_server_process(exe_path);
|
||||
if (!ok) {
|
||||
fprintf(stderr, "[notebook] CreateProcess/fork failed for: %s\n", exe_path);
|
||||
free(found);
|
||||
return false;
|
||||
}
|
||||
free(found);
|
||||
|
||||
// Wait up to 10 seconds for it to become reachable.
|
||||
if (!nb_server_wait_alive(10000)) {
|
||||
fprintf(stderr, "[notebook] server process started but did not become "
|
||||
"reachable within 10 seconds (port 8000 conflict?)\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void nb_server_stop(void) {
|
||||
// The notebook server runs independently; we don't kill it.
|
||||
// It can be stopped by the user or will exit on its own.
|
||||
#ifdef _WIN32
|
||||
if (g_server_process) {
|
||||
CloseHandle(g_server_process);
|
||||
g_server_process = NULL;
|
||||
}
|
||||
#else
|
||||
g_server_pid = 0;
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Server auto-start and lifecycle management.
|
||||
|
||||
#ifndef NB_SERVER_H
|
||||
#define NB_SERVER_H
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
bool nb_server_is_alive(void);
|
||||
|
||||
bool nb_server_ensure(const char *exe_path);
|
||||
|
||||
char *nb_server_find_executable(void);
|
||||
|
||||
bool nb_server_wait_alive(int timeout_ms);
|
||||
|
||||
void nb_server_stop(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif // NB_SERVER_H
|
||||
@@ -0,0 +1,177 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
"golang.org/x/sync/semaphore"
|
||||
)
|
||||
|
||||
// scriptTimeout is the maximum execution time for a single script.
|
||||
const scriptTimeout = 5 * time.Minute
|
||||
|
||||
type JavaScript struct {
|
||||
semaphore *semaphore.Weighted
|
||||
runtime *goja.Runtime
|
||||
rizin *Rizin
|
||||
output strings.Builder
|
||||
}
|
||||
|
||||
func rizinCmd(command string, rz *Rizin) (string, error) {
|
||||
if _, err := rz.exec("e scr.color=0"); err != nil {
|
||||
return "", err
|
||||
}
|
||||
result, err := rz.exec(command)
|
||||
// Restore color regardless of command success.
|
||||
rz.exec("e scr.color=3")
|
||||
return result, err
|
||||
}
|
||||
|
||||
func convertValue(ivalue interface{}) string {
|
||||
switch value := ivalue.(type) {
|
||||
case []interface{}:
|
||||
bytes, _ := json.MarshalIndent(value, "", "\t")
|
||||
return string(bytes)
|
||||
case map[string]interface{}:
|
||||
bytes, _ := json.MarshalIndent(value, "", "\t")
|
||||
return string(bytes)
|
||||
default:
|
||||
return fmt.Sprintf("%v", value)
|
||||
}
|
||||
}
|
||||
|
||||
func NewJavaScript() *JavaScript {
|
||||
runtime := goja.New()
|
||||
if runtime == nil {
|
||||
fmt.Println("error: cannot create JavaScript runtime")
|
||||
return nil
|
||||
}
|
||||
|
||||
sem := semaphore.NewWeighted(1)
|
||||
js := &JavaScript{semaphore: sem, runtime: runtime, rizin: nil}
|
||||
|
||||
// Register the rizin API object.
|
||||
rizinAPI := map[string]interface{}{}
|
||||
rizinAPI["cmd"] = func(args ...interface{}) goja.Value {
|
||||
if js.rizin == nil {
|
||||
panic(js.runtime.ToValue("Rizin pipe is closed."))
|
||||
}
|
||||
if len(args) < 1 {
|
||||
panic(js.runtime.ToValue("No string was passed."))
|
||||
}
|
||||
cmdStr, ok := args[0].(string)
|
||||
if !ok {
|
||||
panic(js.runtime.ToValue("input is not a string."))
|
||||
}
|
||||
result, err := rizinCmd(cmdStr, js.rizin)
|
||||
if err != nil {
|
||||
panic(js.runtime.ToValue(err.Error()))
|
||||
}
|
||||
return js.runtime.ToValue(result)
|
||||
}
|
||||
|
||||
rizinAPI["cmdj"] = func(args ...interface{}) goja.Value {
|
||||
if js.rizin == nil {
|
||||
panic(js.runtime.ToValue("Rizin pipe is closed."))
|
||||
}
|
||||
if len(args) < 1 {
|
||||
panic(js.runtime.ToValue("No string was passed."))
|
||||
}
|
||||
cmdStr, ok := args[0].(string)
|
||||
if !ok {
|
||||
panic(js.runtime.ToValue("input is not a string."))
|
||||
}
|
||||
result, err := rizinCmd(cmdStr, js.rizin)
|
||||
if err != nil {
|
||||
panic(js.runtime.ToValue(err.Error()))
|
||||
}
|
||||
var data interface{}
|
||||
if jsonErr := json.Unmarshal([]byte(result), &data); jsonErr != nil {
|
||||
panic(js.runtime.ToValue(jsonErr.Error()))
|
||||
}
|
||||
return js.runtime.ToValue(data)
|
||||
}
|
||||
|
||||
// Register the console API object.
|
||||
consoleAPI := map[string]interface{}{}
|
||||
|
||||
// writeArgs writes all arguments to js.output separated by spaces.
|
||||
writeArgs := func(prefix string, args []interface{}) {
|
||||
if len(prefix) > 0 {
|
||||
js.output.WriteString(prefix)
|
||||
js.output.WriteString(" ")
|
||||
}
|
||||
for i, value := range args {
|
||||
js.output.WriteString(convertValue(value))
|
||||
if i+1 < len(args) {
|
||||
js.output.WriteString(" ")
|
||||
}
|
||||
}
|
||||
js.output.WriteString("\n")
|
||||
}
|
||||
|
||||
consoleAPI["log"] = func(args ...interface{}) {
|
||||
writeArgs("", args)
|
||||
}
|
||||
|
||||
consoleAPI["warn"] = func(args ...interface{}) {
|
||||
writeArgs("[WARN]", args)
|
||||
}
|
||||
|
||||
consoleAPI["error"] = func(args ...interface{}) {
|
||||
writeArgs("[ERROR]", args)
|
||||
}
|
||||
|
||||
consoleAPI["info"] = func(args ...interface{}) {
|
||||
writeArgs("[INFO]", args)
|
||||
}
|
||||
|
||||
consoleAPI["clear"] = func() {
|
||||
js.output.Reset()
|
||||
}
|
||||
|
||||
consoleAPI["table"] = func(args ...interface{}) {
|
||||
if len(args) < 1 {
|
||||
return
|
||||
}
|
||||
bytes, err := json.MarshalIndent(args[0], "", " ")
|
||||
if err != nil {
|
||||
js.output.WriteString(fmt.Sprintf("[TABLE] %v\n", args[0]))
|
||||
return
|
||||
}
|
||||
js.output.WriteString(string(bytes))
|
||||
js.output.WriteString("\n")
|
||||
}
|
||||
|
||||
runtime.Set("rizin", rizinAPI)
|
||||
runtime.Set("console", consoleAPI)
|
||||
return js
|
||||
}
|
||||
|
||||
func (js *JavaScript) exec(script string, rz *Rizin) (string, error) {
|
||||
if !js.semaphore.TryAcquire(1) {
|
||||
return "", errors.New("a script is already running")
|
||||
}
|
||||
defer js.semaphore.Release(1)
|
||||
|
||||
js.rizin = rz
|
||||
js.output.Reset()
|
||||
|
||||
timer := time.AfterFunc(scriptTimeout, func() {
|
||||
js.runtime.Interrupt("The script execution has timed out.")
|
||||
})
|
||||
defer timer.Stop()
|
||||
|
||||
_, err := js.runtime.RunScript("script.js", script)
|
||||
result := js.output.String()
|
||||
|
||||
// Clean up state for next execution.
|
||||
js.rizin = nil
|
||||
js.output.Reset()
|
||||
|
||||
return result, err
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"html/template"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
notebookSearchSettingsPrefix = "ns:"
|
||||
defaultSearchMaxResults = 200
|
||||
minSearchMaxResults = 10
|
||||
maxSearchMaxResults = 500
|
||||
)
|
||||
|
||||
type notebookSearchConfig struct {
|
||||
MaxResults int `json:"max_results"`
|
||||
DefaultMode NotebookSearchMode `json:"default_mode"`
|
||||
Surface NotebookSearchSurfaceFilter `json:"default_surface"`
|
||||
CellType NotebookSearchCellTypeFilter `json:"default_cell_type"`
|
||||
}
|
||||
|
||||
var notebookSearchSettingOrder = []string{
|
||||
"max_results",
|
||||
"default_mode",
|
||||
"default_surface",
|
||||
"default_cell_type",
|
||||
}
|
||||
|
||||
var notebookSearchSettingLabels = map[string]string{
|
||||
"max_results": "Max Results",
|
||||
"default_mode": "Default Mode",
|
||||
"default_surface": "Default Surface",
|
||||
"default_cell_type": "Default Cell Type",
|
||||
}
|
||||
|
||||
var notebookSearchSettingDefaults = map[string]string{
|
||||
"max_results": strconv.Itoa(defaultSearchMaxResults),
|
||||
"default_mode": string(NotebookSearchModeLiteral),
|
||||
"default_surface": string(NotebookSearchSurfaceFilterAll),
|
||||
"default_cell_type": string(NotebookSearchCellTypeFilterAll),
|
||||
}
|
||||
|
||||
var notebookSearchSettingKinds = map[string]string{
|
||||
"max_results": "number",
|
||||
"default_mode": "select",
|
||||
"default_surface": "select",
|
||||
"default_cell_type": "select",
|
||||
}
|
||||
|
||||
var notebookSearchSettingChoices = map[string][]map[string]string{
|
||||
"default_mode": {
|
||||
{"value": string(NotebookSearchModeLiteral), "label": "Literal"},
|
||||
{"value": string(NotebookSearchModeRegex), "label": "Regex"},
|
||||
{"value": string(NotebookSearchModeFuzzy), "label": "Fuzzy"},
|
||||
},
|
||||
"default_surface": {
|
||||
{"value": string(NotebookSearchSurfaceFilterAll), "label": "All Surfaces"},
|
||||
{"value": string(NotebookSearchSurfaceFilterContent), "label": "Input / Source"},
|
||||
{"value": string(NotebookSearchSurfaceFilterOutput), "label": "Outputs"},
|
||||
},
|
||||
"default_cell_type": {
|
||||
{"value": string(NotebookSearchCellTypeFilterAll), "label": "All Cell Types"},
|
||||
{"value": string(NotebookSearchCellTypeFilterCommand), "label": "Command Cells"},
|
||||
{"value": string(NotebookSearchCellTypeFilterScript), "label": "Script Cells"},
|
||||
{"value": string(NotebookSearchCellTypeFilterMarkdown), "label": "Markdown Cells"},
|
||||
},
|
||||
}
|
||||
|
||||
func getNotebookSearchConfig() notebookSearchConfig {
|
||||
cfg := notebookSearchConfig{
|
||||
MaxResults: defaultSearchMaxResults,
|
||||
DefaultMode: NotebookSearchModeLiteral,
|
||||
Surface: NotebookSearchSurfaceFilterAll,
|
||||
CellType: NotebookSearchCellTypeFilterAll,
|
||||
}
|
||||
|
||||
if catalog == nil {
|
||||
return cfg
|
||||
}
|
||||
|
||||
settings, _ := catalog.GetAllSettings()
|
||||
if settings == nil {
|
||||
return cfg
|
||||
}
|
||||
|
||||
for key, value := range settings {
|
||||
if !strings.HasPrefix(key, notebookSearchSettingsPrefix) {
|
||||
continue
|
||||
}
|
||||
settingKey := strings.TrimPrefix(key, notebookSearchSettingsPrefix)
|
||||
switch settingKey {
|
||||
case "max_results":
|
||||
val, err := strconv.Atoi(value)
|
||||
if err == nil {
|
||||
cfg.MaxResults = clampInt(val, minSearchMaxResults, maxSearchMaxResults)
|
||||
}
|
||||
case "default_mode":
|
||||
mode, err := normalizeNotebookSearchMode(NotebookSearchMode(value))
|
||||
if err == nil {
|
||||
cfg.DefaultMode = mode
|
||||
}
|
||||
case "default_surface":
|
||||
surface, err := normalizeNotebookSearchSurfaceFilter(NotebookSearchSurfaceFilter(value))
|
||||
if err == nil {
|
||||
cfg.Surface = surface
|
||||
}
|
||||
case "default_cell_type":
|
||||
cellType, err := normalizeNotebookSearchCellTypeFilter(NotebookSearchCellTypeFilter(value))
|
||||
if err == nil {
|
||||
cfg.CellType = cellType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
func getNotebookSearchConfigJSON() template.JS {
|
||||
cfg := getNotebookSearchConfig()
|
||||
bytes, err := json.Marshal(cfg)
|
||||
if err != nil {
|
||||
return template.JS("{}")
|
||||
}
|
||||
return template.JS(bytes)
|
||||
}
|
||||
|
||||
func notebookSearchActions() []map[string]string {
|
||||
cfg := getNotebookSearchConfig()
|
||||
actions := make([]map[string]string, 0, len(notebookSearchSettingOrder))
|
||||
for _, key := range notebookSearchSettingOrder {
|
||||
value := notebookSearchSettingDefaults[key]
|
||||
switch key {
|
||||
case "max_results":
|
||||
value = strconv.Itoa(cfg.MaxResults)
|
||||
case "default_mode":
|
||||
value = string(cfg.DefaultMode)
|
||||
case "default_surface":
|
||||
value = string(cfg.Surface)
|
||||
case "default_cell_type":
|
||||
value = string(cfg.CellType)
|
||||
}
|
||||
actions = append(actions, map[string]string{
|
||||
"name": key,
|
||||
"label": notebookSearchSettingLabels[key],
|
||||
"value": value,
|
||||
"value_display": notebookSearchSettingValueLabel(key, value),
|
||||
"default_value": notebookSearchSettingDefaults[key],
|
||||
"default_display": notebookSearchSettingValueLabel(key, notebookSearchSettingDefaults[key]),
|
||||
})
|
||||
}
|
||||
return actions
|
||||
}
|
||||
|
||||
func notebookSearchSettingValueLabel(key, value string) string {
|
||||
choices, ok := notebookSearchSettingChoices[key]
|
||||
if !ok {
|
||||
return value
|
||||
}
|
||||
for _, choice := range choices {
|
||||
if choice["value"] == value {
|
||||
return choice["label"]
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
@@ -1,22 +1,20 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func sanitizeWebRoot(path string) string {
|
||||
if len(path) < 2 {
|
||||
return "/"
|
||||
} else if len(path) > 1 && path[len(path)-1] != '/' {
|
||||
}
|
||||
if path[len(path)-1] != '/' {
|
||||
return path + "/"
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func runServer(assets, bind string, debug bool) {
|
||||
var root *gin.RouterGroup
|
||||
|
||||
func setupRouter(assets, bind string, debug bool) *gin.Engine {
|
||||
gin.DisableConsoleColor()
|
||||
if debug {
|
||||
gin.SetMode(gin.DebugMode)
|
||||
@@ -28,21 +26,38 @@ func runServer(assets, bind string, debug bool) {
|
||||
|
||||
static, templates := setupTemplate(assets, router)
|
||||
|
||||
root = router.Group(sanitizeWebRoot(webroot))
|
||||
root := router.Group(sanitizeWebRoot(webroot))
|
||||
|
||||
serverAddAssets(root, assets, static, templates)
|
||||
|
||||
// Index page: list all notebook pages
|
||||
root.GET("/", func(c *gin.Context) {
|
||||
pages, err := catalog.ListPages()
|
||||
if err != nil {
|
||||
c.HTML(500, "error.tmpl", gin.H{"root": webroot, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
list := make([]gin.H, len(pages))
|
||||
for i, p := range pages {
|
||||
notebook.mutex.Lock()
|
||||
pipeOpen := notebook.pipes[p.ID] != nil
|
||||
notebook.mutex.Unlock()
|
||||
list[i] = gin.H{
|
||||
"title": p.Title,
|
||||
"unique": p.ID,
|
||||
"pipe": pipeOpen,
|
||||
}
|
||||
}
|
||||
|
||||
c.HTML(200, "index.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"list": notebook.list(),
|
||||
"list": list,
|
||||
})
|
||||
})
|
||||
|
||||
serverAddAbout(root)
|
||||
|
||||
serverAddSettings(root)
|
||||
|
||||
serverAddPage(root)
|
||||
|
||||
pipe := root.Group("/pipe")
|
||||
@@ -54,6 +69,8 @@ func runServer(assets, bind string, debug bool) {
|
||||
output := root.Group("/output")
|
||||
serverAddOutput(output)
|
||||
|
||||
fmt.Printf("Server listening at http://%s\n", bind)
|
||||
router.Run(bind)
|
||||
// Protobuf REST API.
|
||||
serverAddAPI(root)
|
||||
|
||||
return router
|
||||
}
|
||||
@@ -1,37 +1,36 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func serverAddAbout(root *gin.RouterGroup) {
|
||||
root.GET("/about", func(c *gin.Context) {
|
||||
var rzversion, rzbuild string
|
||||
info, err := notebook.info()
|
||||
rzpath := notebook.rizin
|
||||
|
||||
if err != nil {
|
||||
rzversion = err.Error()
|
||||
rzbuild = rzversion
|
||||
if rzpath == "rizin" {
|
||||
rzpath = "Not found in $PATH"
|
||||
} else {
|
||||
rzpath = "Not found in " + rzpath
|
||||
}
|
||||
} else {
|
||||
if info, err := notebook.info(); err == nil && len(info) > 0 {
|
||||
rzversion = info[0]
|
||||
rzbuild = info[1]
|
||||
if rzpath == "rizin" {
|
||||
rzpath = "Available from $PATH"
|
||||
if len(info) > 1 {
|
||||
rzbuild = info[1]
|
||||
}
|
||||
}
|
||||
|
||||
rzpath := os.Getenv("RIZIN_PATH")
|
||||
if len(rzpath) < 1 {
|
||||
rzpath = notebook.rizin
|
||||
}
|
||||
if resolved, err := exec.LookPath(rzpath); err == nil {
|
||||
rzpath = resolved
|
||||
}
|
||||
|
||||
c.HTML(200, "about.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"nbversion": NBVERSION,
|
||||
"rzversion": rzversion,
|
||||
"rzbuild": rzbuild,
|
||||
"rzpath": rzpath,
|
||||
"nbversion": NBVERSION,
|
||||
"storage": notebook.storage,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,122 +1,176 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"html/template"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"path"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin/render"
|
||||
)
|
||||
|
||||
//go:embed assets/templates/*
|
||||
var embedTemplates embed.FS
|
||||
|
||||
//go:embed assets/static/*
|
||||
var embedStatic embed.FS
|
||||
|
||||
// Template helper functions: stringify, keybindings.
|
||||
var functionMap = template.FuncMap{
|
||||
"raw": func(b []byte) template.HTML {
|
||||
return template.HTML(b)
|
||||
},
|
||||
"stringify": func(input interface{}) string {
|
||||
buffer, err := json.Marshal(input)
|
||||
"stringify": func(v interface{}) string {
|
||||
bytes, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return `""`
|
||||
return "{}"
|
||||
}
|
||||
return string(buffer)
|
||||
return string(bytes)
|
||||
},
|
||||
"keycombo": func(input string) string {
|
||||
if len(input) > 0 {
|
||||
return strings.Replace(input, ",", " + ", -1)
|
||||
}
|
||||
return "Not Assigned"
|
||||
"keybindings": func() template.JS {
|
||||
return getKeybindingsJSON()
|
||||
},
|
||||
"autocomplete_config": func() template.JS {
|
||||
return getAutocompleteConfigJSON()
|
||||
},
|
||||
"search_config": func() template.JS {
|
||||
return getNotebookSearchConfigJSON()
|
||||
},
|
||||
}
|
||||
|
||||
func loadEmbedded() (*template.Template, error) {
|
||||
t := template.New("")
|
||||
for name, file := range Assets.Files {
|
||||
if file.IsDir() || !strings.HasSuffix(name, ".tmpl") {
|
||||
continue
|
||||
}
|
||||
h, err := ioutil.ReadAll(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t, err = t.New(path.Base(name)).Funcs(functionMap).Parse(string(h))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return t, nil
|
||||
/* ─── Custom template render ─────────────────────────────── */
|
||||
|
||||
type customHTMLRender struct {
|
||||
Template *template.Template
|
||||
}
|
||||
|
||||
func loadAsset(file string) ([]byte, error) {
|
||||
asset, err := Assets.Open("/assets/static/" + file)
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
func (r *customHTMLRender) Instance(name string, data interface{}) render.Render {
|
||||
return &customHTMLRenderInstance{
|
||||
Template: r.Template,
|
||||
Name: name,
|
||||
Data: data,
|
||||
}
|
||||
content, err := ioutil.ReadAll(asset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return content, nil
|
||||
}
|
||||
|
||||
func setupTemplate(assets string, router *gin.Engine) (string, string) {
|
||||
var static, templates string
|
||||
type customHTMLRenderInstance struct {
|
||||
Template *template.Template
|
||||
Name string
|
||||
Data interface{}
|
||||
}
|
||||
|
||||
func (r *customHTMLRenderInstance) Render(w http.ResponseWriter) error {
|
||||
r.WriteContentType(w)
|
||||
return r.Template.ExecuteTemplate(w, r.Name, r.Data)
|
||||
}
|
||||
|
||||
func (r *customHTMLRenderInstance) WriteContentType(w http.ResponseWriter) {
|
||||
header := w.Header()
|
||||
if val := header["Content-Type"]; len(val) == 0 {
|
||||
header["Content-Type"] = []string{"text/html; charset=utf-8"}
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Template loading ───────────────────────────────────── */
|
||||
|
||||
func setupTemplate(assets string, router *gin.Engine) (http.FileSystem, *template.Template) {
|
||||
if len(assets) > 0 {
|
||||
fmt.Println("debugging assets.")
|
||||
static = path.Join(assets, "static")
|
||||
templates = path.Join(assets, "templates", "*")
|
||||
router.SetFuncMap(functionMap)
|
||||
router.LoadHTMLGlob(templates)
|
||||
} else {
|
||||
templates, err := loadEmbedded()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
router.SetHTMLTemplate(templates)
|
||||
return setupTemplateDebug(assets, router)
|
||||
}
|
||||
return static, templates
|
||||
return setupTemplateEmbed(router)
|
||||
}
|
||||
|
||||
func serverAddAssets(root *gin.RouterGroup, assets, static, templates string) {
|
||||
if len(assets) > 0 {
|
||||
root.GET("/favicon.ico", func(c *gin.Context) {
|
||||
c.Redirect(302, "/static/favicon.ico")
|
||||
})
|
||||
root.Static("/static", static)
|
||||
} else {
|
||||
root.GET("/favicon.ico", func(c *gin.Context) {
|
||||
content, err := loadAsset("favicon.ico")
|
||||
if content == nil && err == nil {
|
||||
c.Status(404)
|
||||
return
|
||||
} else if err != nil {
|
||||
c.Status(500)
|
||||
fmt.Println("[Assets]", err)
|
||||
return
|
||||
}
|
||||
c.Data(200, "image/x-icon", content)
|
||||
})
|
||||
root.GET("/static/:file", func(c *gin.Context) {
|
||||
file := c.Param("file")
|
||||
content, err := loadAsset(file)
|
||||
if content == nil && err == nil {
|
||||
c.Status(404)
|
||||
return
|
||||
} else if err != nil {
|
||||
c.Status(500)
|
||||
fmt.Println("[Assets]", err)
|
||||
return
|
||||
}
|
||||
var contentType = "text/plain"
|
||||
if strings.HasSuffix(file, ".css") {
|
||||
contentType = "text/css"
|
||||
} else if strings.HasSuffix(file, ".js") {
|
||||
contentType = "text/javascript"
|
||||
} else {
|
||||
contentType = http.DetectContentType(content)
|
||||
}
|
||||
c.Data(200, contentType, content)
|
||||
})
|
||||
func setupTemplateEmbed(router *gin.Engine) (http.FileSystem, *template.Template) {
|
||||
tmpl := template.New("").Funcs(functionMap)
|
||||
|
||||
err := fs.WalkDir(embedTemplates, "assets/templates", func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d.IsDir() || !strings.HasSuffix(path, ".tmpl") {
|
||||
return nil
|
||||
}
|
||||
data, readErr := embedTemplates.ReadFile(path)
|
||||
if readErr != nil {
|
||||
return readErr
|
||||
}
|
||||
name := filepath.Base(path)
|
||||
_, parseErr := tmpl.New(name).Parse(string(data))
|
||||
return parseErr
|
||||
})
|
||||
if err != nil {
|
||||
panic("failed to load embedded templates: " + err.Error())
|
||||
}
|
||||
|
||||
router.HTMLRender = &customHTMLRender{Template: tmpl}
|
||||
|
||||
staticFS, fsErr := fs.Sub(embedStatic, "assets/static")
|
||||
if fsErr != nil {
|
||||
panic("failed to load embedded static assets: " + fsErr.Error())
|
||||
}
|
||||
return http.FS(staticFS), tmpl
|
||||
}
|
||||
|
||||
func setupTemplateDebug(assets string, router *gin.Engine) (http.FileSystem, *template.Template) {
|
||||
tmpl := template.New("").Funcs(functionMap)
|
||||
templateDir := filepath.Join(assets, "templates")
|
||||
|
||||
err := filepath.Walk(templateDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.IsDir() || !strings.HasSuffix(path, ".tmpl") {
|
||||
return nil
|
||||
}
|
||||
data, readErr := os.ReadFile(path)
|
||||
if readErr != nil {
|
||||
return readErr
|
||||
}
|
||||
name := filepath.Base(path)
|
||||
_, parseErr := tmpl.New(name).Parse(string(data))
|
||||
return parseErr
|
||||
})
|
||||
if err != nil {
|
||||
panic("failed to load debug templates: " + err.Error())
|
||||
}
|
||||
|
||||
router.HTMLRender = &customHTMLRender{Template: tmpl}
|
||||
|
||||
staticDir := filepath.Join(assets, "static")
|
||||
return http.Dir(staticDir), tmpl
|
||||
}
|
||||
|
||||
/* ─── Static file routes ─────────────────────────────────── */
|
||||
|
||||
func serverAddAssets(root *gin.RouterGroup, assets string, static http.FileSystem, templates *template.Template) {
|
||||
root.StaticFS("/static", static)
|
||||
root.GET("/favicon.ico", func(c *gin.Context) {
|
||||
c.Writer.WriteHeader(http.StatusNoContent)
|
||||
_, _ = io.WriteString(c.Writer, "")
|
||||
})
|
||||
|
||||
root.GET("/reload", func(c *gin.Context) {
|
||||
if len(assets) > 0 {
|
||||
templateDir := filepath.Join(assets, "templates")
|
||||
newTmpl := template.New("").Funcs(functionMap)
|
||||
_ = filepath.Walk(templateDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil || info.IsDir() || !strings.HasSuffix(path, ".tmpl") {
|
||||
return err
|
||||
}
|
||||
data, readErr := os.ReadFile(path)
|
||||
if readErr != nil {
|
||||
return readErr
|
||||
}
|
||||
name := filepath.Base(path)
|
||||
_, parseErr := newTmpl.New(name).Parse(string(data))
|
||||
return parseErr
|
||||
})
|
||||
templates = newTmpl
|
||||
}
|
||||
c.HTML(200, "reload.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -1,94 +1,147 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func serverAddMarkdown(markdown *gin.RouterGroup) {
|
||||
markdown.GET("/deleted", func(c *gin.Context) {
|
||||
c.String(200, "deleted.")
|
||||
})
|
||||
markdown.GET("/view/*path", func(c *gin.Context) {
|
||||
path := c.Param("path")
|
||||
tokens := strings.Split(path[1:], "/")
|
||||
if len(tokens) != 2 || !IsValidNonce(tokens[0]) || !IsValidNonce(tokens[1]) {
|
||||
c.String(400, "invalid request")
|
||||
} else {
|
||||
bytes, err := notebook.file(tokens[0], tokens[1]+".md")
|
||||
if err != nil {
|
||||
c.String(404, "file not found")
|
||||
} else {
|
||||
c.HTML(200, "markdown-view.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"html": string(bytes),
|
||||
"path": "/" + tokens[0] + "/" + tokens[1],
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
markdown.GET("/edit/*path", func(c *gin.Context) {
|
||||
path := c.Param("path")
|
||||
tokens := strings.Split(path[1:], "/")
|
||||
if len(tokens) != 2 || !IsValidNonce(tokens[0]) || !IsValidNonce(tokens[1]) {
|
||||
c.String(400, "invalid request")
|
||||
} else {
|
||||
bytes, err := notebook.file(tokens[0], tokens[1]+".md")
|
||||
if err != nil {
|
||||
c.String(404, "file not found")
|
||||
} else {
|
||||
c.HTML(200, "markdown-edit.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"raw": bytes,
|
||||
"path": "/" + tokens[0] + "/" + tokens[1],
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
markdown.GET("/delete/*path", func(c *gin.Context) {
|
||||
path := c.Param("path")
|
||||
tokens := strings.Split(path[1:], "/")
|
||||
if len(tokens) != 2 || !IsValidNonce(tokens[0]) || !IsValidNonce(tokens[1]) {
|
||||
c.String(400, "invalid request")
|
||||
} else {
|
||||
if !notebook.deleteElem(tokens[0], tokens[1], true) {
|
||||
c.String(404, "file not found")
|
||||
} else {
|
||||
c.Redirect(302, webroot+"markdown/deleted")
|
||||
}
|
||||
}
|
||||
})
|
||||
markdown.POST("/save/*path", func(c *gin.Context) {
|
||||
path := c.Param("path")
|
||||
tokens := strings.Split(path[1:], "/")
|
||||
if len(tokens) != 2 || !IsValidNonce(tokens[0]) || !IsValidNonce(tokens[1]) {
|
||||
c.String(400, "invalid request")
|
||||
} else {
|
||||
markdown := c.DefaultPostForm("markdown", "")
|
||||
if !notebook.save([]byte(markdown), tokens[0], tokens[1]+".md") {
|
||||
c.String(500, "can't save markdown.")
|
||||
} else {
|
||||
c.Redirect(302, webroot+"markdown/view/"+tokens[0]+"/"+tokens[1])
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Create a new markdown cell.
|
||||
markdown.GET("/new/:unique", func(c *gin.Context) {
|
||||
unique := c.Param("unique")
|
||||
page := notebook.get(unique)
|
||||
if page == nil {
|
||||
c.HTML(404, "error.tmpl", gin.H{
|
||||
if !IsValidNonce(unique, PageNonceSize) {
|
||||
c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"error": "cannot find a notebook page",
|
||||
"error": "invalid page identifier",
|
||||
})
|
||||
} else {
|
||||
if eunique := notebook.newmd(unique); len(eunique) > 0 {
|
||||
c.Redirect(302, webroot+"markdown/edit/"+unique+"/"+eunique)
|
||||
} else {
|
||||
c.HTML(404, "error.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"error": "cannot add element to the page",
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
eunique := Nonce(ElementNonceSize)
|
||||
if err := catalog.AddCell(unique, eunique, "markdown", ""); err != nil {
|
||||
c.HTML(http.StatusNotFound, "error.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"error": "failed to create markdown cell",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusFound, webroot+"markdown/edit/"+unique+"/"+eunique)
|
||||
})
|
||||
|
||||
// View a rendered markdown cell.
|
||||
markdown.GET("/view/*path", func(c *gin.Context) {
|
||||
unique, eunique, ok := parseElementPath(c.Param("path"))
|
||||
if !ok {
|
||||
c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"error": "invalid path",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
cell, _ := catalog.GetCell(unique, eunique)
|
||||
var content string
|
||||
if cell != nil {
|
||||
content = cell.Content
|
||||
}
|
||||
|
||||
c.HTML(200, "markdown-view.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"path": "/" + unique + "/" + eunique,
|
||||
"raw": content,
|
||||
})
|
||||
})
|
||||
|
||||
// Edit a markdown cell.
|
||||
markdown.GET("/edit/*path", func(c *gin.Context) {
|
||||
unique, eunique, ok := parseElementPath(c.Param("path"))
|
||||
if !ok {
|
||||
c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"error": "invalid path",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
cell, _ := catalog.GetCell(unique, eunique)
|
||||
var content []byte
|
||||
if cell != nil {
|
||||
content = []byte(cell.Content)
|
||||
}
|
||||
|
||||
c.HTML(200, "markdown-edit.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"path": "/" + unique + "/" + eunique,
|
||||
"raw": content,
|
||||
})
|
||||
})
|
||||
|
||||
// Save markdown cell content.
|
||||
markdown.POST("/save/*path", func(c *gin.Context) {
|
||||
unique, eunique, ok := parseElementPath(c.Param("path"))
|
||||
if !ok {
|
||||
c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"error": "invalid path",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
content := c.PostForm("markdown")
|
||||
catalog.UpdateCellContent(unique, eunique, content)
|
||||
|
||||
c.Redirect(http.StatusFound, webroot+"markdown/view/"+unique+"/"+eunique)
|
||||
})
|
||||
|
||||
// Delete a markdown cell.
|
||||
markdown.GET("/delete/*path", func(c *gin.Context) {
|
||||
unique, eunique, ok := parseElementPath(c.Param("path"))
|
||||
if !ok {
|
||||
c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"error": "invalid path",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
catalog.DeleteCell(unique, eunique)
|
||||
|
||||
// Redirect to "deleted" pseudo-page so iframe can detect removal.
|
||||
c.Redirect(http.StatusFound, webroot+"markdown/deleted")
|
||||
})
|
||||
|
||||
// Pseudo-page for iframe deletion detection.
|
||||
markdown.GET("/deleted", func(c *gin.Context) {
|
||||
c.String(200, "deleted")
|
||||
})
|
||||
}
|
||||
|
||||
/* ─── Helpers ─────────────────────────────────────────────── */
|
||||
|
||||
func parseElementPath(path string) (string, string, bool) {
|
||||
parts := splitPath(path)
|
||||
if len(parts) != 2 {
|
||||
return "", "", false
|
||||
}
|
||||
unique := parts[0]
|
||||
eunique := parts[1]
|
||||
if !IsValidNonce(unique, PageNonceSize) || !IsValidNonce(eunique, ElementNonceSize) {
|
||||
return "", "", false
|
||||
}
|
||||
return unique, eunique, true
|
||||
}
|
||||
|
||||
func splitPath(path string) []string {
|
||||
raw := strings.Split(path, "/")
|
||||
var parts []string
|
||||
for _, p := range raw {
|
||||
if p != "" {
|
||||
parts = append(parts, p)
|
||||
}
|
||||
}
|
||||
return parts
|
||||
}
|
||||
@@ -2,128 +2,285 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func formatExecutionDuration(duration time.Duration) string {
|
||||
if duration < time.Millisecond {
|
||||
return duration.Round(time.Microsecond).String()
|
||||
}
|
||||
if duration < time.Second {
|
||||
return duration.Round(time.Millisecond).String()
|
||||
}
|
||||
return duration.Round(time.Millisecond).String()
|
||||
}
|
||||
|
||||
func formatActionTime(unixTime int64) string {
|
||||
if unixTime <= 0 {
|
||||
return ""
|
||||
}
|
||||
return time.Unix(unixTime, 0).Local().Format("15:04")
|
||||
}
|
||||
|
||||
func serverAddOutput(output *gin.RouterGroup) {
|
||||
output.GET("/deleted", func(c *gin.Context) {
|
||||
c.String(200, "deleted.")
|
||||
|
||||
// View a rendered output cell.
|
||||
output.GET("/view/*path", func(c *gin.Context) {
|
||||
unique, eunique, ok := parseElementPath(c.Param("path"))
|
||||
if !ok {
|
||||
c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"error": "invalid path",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
cell, _ := catalog.GetCell(unique, eunique)
|
||||
var data []byte
|
||||
var lines, words int
|
||||
var actionTime string
|
||||
var visibleOutput string
|
||||
if cell != nil {
|
||||
data = cell.Output
|
||||
visibleOutput = outputVisibleText(string(data))
|
||||
if len(visibleOutput) > 0 {
|
||||
lines = strings.Count(visibleOutput, "\n") + 1
|
||||
words = len(strings.Fields(visibleOutput))
|
||||
}
|
||||
if cell.Executed > 0 {
|
||||
actionTime = formatActionTime(cell.Executed)
|
||||
} else {
|
||||
actionTime = formatActionTime(cell.Created)
|
||||
}
|
||||
}
|
||||
|
||||
htmlOutput := toHtml(string(data))
|
||||
|
||||
c.HTML(200, "output.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"output": template.HTML(string(htmlOutput)),
|
||||
"raw_output": visibleOutput,
|
||||
"lines": lines,
|
||||
"words": words,
|
||||
"actionTime": actionTime,
|
||||
})
|
||||
})
|
||||
output.GET("/loaded", func(c *gin.Context) {
|
||||
c.String(200, "found.")
|
||||
|
||||
// Delete an output cell.
|
||||
output.GET("/delete/*path", func(c *gin.Context) {
|
||||
unique, eunique, ok := parseElementPath(c.Param("path"))
|
||||
if !ok {
|
||||
c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"error": "invalid path",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
catalog.DeleteCell(unique, eunique)
|
||||
|
||||
c.Redirect(http.StatusFound, webroot+"output/deleted")
|
||||
})
|
||||
|
||||
// Command line input form (rendered in an iframe).
|
||||
output.GET("/input/console/:unique", func(c *gin.Context) {
|
||||
unique := c.Param("unique")
|
||||
if !IsValidNonce(unique, PageNonceSize) {
|
||||
c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"error": "invalid page identifier",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.HTML(200, "console.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"unique": unique,
|
||||
})
|
||||
})
|
||||
|
||||
// Script editor input form (rendered in an iframe).
|
||||
output.GET("/input/script/:unique", func(c *gin.Context) {
|
||||
unique := c.Param("unique")
|
||||
if !IsValidNonce(unique, PageNonceSize) {
|
||||
c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"error": "invalid page identifier",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.HTML(200, "script.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"unique": unique,
|
||||
})
|
||||
})
|
||||
|
||||
// Execute a Rizin command.
|
||||
output.POST("/exec/:unique", func(c *gin.Context) {
|
||||
unique := c.Param("unique")
|
||||
command := c.DefaultPostForm("command", "")
|
||||
if !IsValidNonce(unique) || len(command) < 1 {
|
||||
c.HTML(400, "console-error.tmpl", gin.H{
|
||||
"error": "invalid request",
|
||||
if !IsValidNonce(unique, PageNonceSize) {
|
||||
c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"error": "invalid page identifier",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
rizin := notebook.open(unique, false)
|
||||
if rizin == nil {
|
||||
c.HTML(400, "console-error.tmpl", gin.H{
|
||||
"error": "pipe is closed.",
|
||||
"root": webroot,
|
||||
command := c.PostForm("command")
|
||||
if command == "" {
|
||||
c.Redirect(http.StatusFound, webroot+"output/deleted")
|
||||
return
|
||||
}
|
||||
|
||||
// Get or open the pipe.
|
||||
rz := notebook.open(unique, true)
|
||||
if rz == nil {
|
||||
c.HTML(http.StatusInternalServerError, "console.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"unique": unique,
|
||||
"command": command,
|
||||
"error": "Failed to open Rizin pipe",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
section := notebook.newcmd(unique, command)
|
||||
if len(section) < 1 {
|
||||
c.HTML(400, "console-error.tmpl", gin.H{
|
||||
"error": "cannot create section for output.",
|
||||
"root": webroot,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
go func(unique, name, command string, rizin *Rizin) {
|
||||
output, err := rizin.exec(command)
|
||||
if err != nil {
|
||||
output = fmt.Sprintf("pipe error: %v", err)
|
||||
}
|
||||
notebook.save([]byte(output), unique, name+".out")
|
||||
}(unique, section, command, rizin)
|
||||
|
||||
c.Redirect(302, webroot+"output/check/"+unique+"/"+section)
|
||||
})
|
||||
output.GET("/check/*path", func(c *gin.Context) {
|
||||
tokens := strings.Split(c.Param("path")[1:], "/")
|
||||
if len(tokens) != 2 || !IsValidNonce(tokens[0]) || !IsValidNonce(tokens[1]) {
|
||||
c.HTML(400, "error.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"error": "invalid request",
|
||||
})
|
||||
} else {
|
||||
_, err := notebook.file(tokens[0], tokens[1]+".out")
|
||||
if err != nil {
|
||||
c.HTML(200, "reload.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
})
|
||||
} else {
|
||||
c.Redirect(302, webroot+"output/loaded")
|
||||
}
|
||||
}
|
||||
})
|
||||
output.GET("/input/:unique", func(c *gin.Context) {
|
||||
unique := c.Param("unique")
|
||||
if !IsValidNonce(unique) {
|
||||
c.HTML(400, "error.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"error": "invalid request",
|
||||
})
|
||||
} else {
|
||||
// Execute the command.
|
||||
started := time.Now()
|
||||
result, err := rz.exec(command)
|
||||
if err != nil {
|
||||
c.HTML(200, "console.tmpl", gin.H{
|
||||
"unique": unique,
|
||||
"root": webroot,
|
||||
"root": webroot,
|
||||
"unique": unique,
|
||||
"command": command,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
})
|
||||
output.GET("/view/*path", func(c *gin.Context) {
|
||||
tokens := strings.Split(c.Param("path")[1:], "/")
|
||||
if len(tokens) != 2 || !IsValidNonce(tokens[0]) || !IsValidNonce(tokens[1]) {
|
||||
c.HTML(400, "output.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"output": []byte("invalid request"),
|
||||
|
||||
// Create a command cell and save the output in SQLite.
|
||||
eunique := Nonce(ElementNonceSize)
|
||||
if storeErr := catalog.AddCell(unique, eunique, "command", command); storeErr != nil {
|
||||
c.HTML(http.StatusInternalServerError, "console.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"unique": unique,
|
||||
"command": command,
|
||||
"error": "Failed to create command element",
|
||||
})
|
||||
} else {
|
||||
bytes, err := notebook.file(tokens[0], tokens[1]+".out")
|
||||
if err != nil {
|
||||
c.HTML(404, "output.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"output": []byte("missing output file"),
|
||||
})
|
||||
} else {
|
||||
output := toHtml(bytes)
|
||||
c.HTML(200, "output.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"output": output,
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
catalog.UpdateCellOutput(unique, eunique, []byte(result))
|
||||
durationText := formatExecutionDuration(time.Since(started))
|
||||
lineCount := 0
|
||||
wordCount := 0
|
||||
if result != "" {
|
||||
lineCount = strings.Count(result, "\n") + 1
|
||||
wordCount = len(strings.Fields(result))
|
||||
}
|
||||
actionTime := time.Now().Local().Format("15:04")
|
||||
|
||||
c.HTML(http.StatusOK, "console.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"unique": unique,
|
||||
"command": command,
|
||||
"message": fmt.Sprintf("Command executed in %s", durationText),
|
||||
"lineCount": lineCount,
|
||||
"wordCount": wordCount,
|
||||
"actionTime": actionTime,
|
||||
"cellUnique": eunique,
|
||||
})
|
||||
})
|
||||
output.GET("/delete/*path", func(c *gin.Context) {
|
||||
path := c.Param("path")
|
||||
tokens := strings.Split(path[1:], "/")
|
||||
if len(tokens) != 2 || !IsValidNonce(tokens[0]) || !IsValidNonce(tokens[1]) {
|
||||
c.HTML(400, "error.tmpl", gin.H{
|
||||
|
||||
// Execute a JavaScript script.
|
||||
output.POST("/script/:unique", func(c *gin.Context) {
|
||||
unique := c.Param("unique")
|
||||
if !IsValidNonce(unique, PageNonceSize) {
|
||||
c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"error": "invalid request",
|
||||
"error": "invalid page identifier",
|
||||
})
|
||||
} else {
|
||||
if notebook.deleteElem(tokens[0], tokens[1], false) {
|
||||
c.Redirect(302, webroot+"view/"+tokens[0])
|
||||
} else {
|
||||
c.HTML(400, "error.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"error": "invalid request",
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
script := c.PostForm("script")
|
||||
if script == "" {
|
||||
c.Redirect(http.StatusFound, webroot+"output/deleted")
|
||||
return
|
||||
}
|
||||
|
||||
// Get or open the pipe.
|
||||
rz := notebook.open(unique, true)
|
||||
|
||||
// Create a script cell in SQLite.
|
||||
eunique := Nonce(ElementNonceSize)
|
||||
if storeErr := catalog.AddCell(unique, eunique, "script", script); storeErr != nil {
|
||||
c.HTML(http.StatusInternalServerError, "script.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"unique": unique,
|
||||
"script": script,
|
||||
"error": "Failed to create script element",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Execute the script. rz may be nil if no pipe is open,
|
||||
// which is handled by the JavaScript engine's rizin.cmd error.
|
||||
started := time.Now()
|
||||
result, err := notebook.jsvm.exec(script, rz)
|
||||
|
||||
if err != nil {
|
||||
catalog.UpdateCellOutput(unique, eunique, []byte(err.Error()))
|
||||
c.HTML(200, "script.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"unique": unique,
|
||||
"script": script,
|
||||
"error": err.Error(),
|
||||
"cellUnique": eunique,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
catalog.UpdateCellOutput(unique, eunique, []byte(result))
|
||||
durationText := formatExecutionDuration(time.Since(started))
|
||||
lineCount := 0
|
||||
wordCount := 0
|
||||
if result != "" {
|
||||
lineCount = strings.Count(result, "\n") + 1
|
||||
wordCount = len(strings.Fields(result))
|
||||
}
|
||||
actionTime := time.Now().Local().Format("15:04")
|
||||
|
||||
c.HTML(http.StatusOK, "script.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"unique": unique,
|
||||
"script": script,
|
||||
"message": fmt.Sprintf("Script executed in %s", durationText),
|
||||
"lineCount": lineCount,
|
||||
"wordCount": wordCount,
|
||||
"actionTime": actionTime,
|
||||
"cellUnique": eunique,
|
||||
})
|
||||
})
|
||||
|
||||
// Pseudo-page for iframe deletion detection.
|
||||
// When the iframe navigates here, the parent page's output() handler
|
||||
// removes the iframe's container div from the DOM.
|
||||
output.GET("/deleted", func(c *gin.Context) {
|
||||
c.String(200, "deleted")
|
||||
})
|
||||
|
||||
// Pseudo-page for parent page reload trigger.
|
||||
// When the iframe navigates here, the parent page's output() handler
|
||||
// calls location.reload() to refresh the entire page.
|
||||
output.GET("/loaded", func(c *gin.Context) {
|
||||
c.HTML(200, "reload.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -1,116 +1,215 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"path"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func serverAddPage(root *gin.RouterGroup) {
|
||||
|
||||
// Show new page form (empty unique).
|
||||
root.GET("/new", func(c *gin.Context) {
|
||||
c.HTML(200, "page-new.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"title": "",
|
||||
"page": gin.H{},
|
||||
"unique": "",
|
||||
"title": "",
|
||||
})
|
||||
})
|
||||
root.POST("/edit", func(c *gin.Context) {
|
||||
unique := c.DefaultPostForm("unique", "")
|
||||
title := c.DefaultPostForm("title", "")
|
||||
if len(unique) > 0 {
|
||||
valid := IsValidNonce(unique)
|
||||
if valid && notebook.rename(unique, title) {
|
||||
c.Redirect(302, webroot+"view/"+unique)
|
||||
} else {
|
||||
c.HTML(500, "error.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"error": "cannot rename page",
|
||||
})
|
||||
}
|
||||
} else {
|
||||
file, err := c.FormFile("binary")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
c.HTML(500, "error.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"error": "invalid or missing binary",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
filename := filepath.Base(file.Filename)
|
||||
if len(filename) < 1 {
|
||||
c.HTML(500, "error.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"error": "invalid binary name",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
ext := filepath.Ext(filename)
|
||||
binary := Nonce(ELEMENT_NONCE_SIZE) + ext
|
||||
unique := notebook.new(title, filename, binary)
|
||||
if len(unique) < 1 {
|
||||
c.HTML(500, "error.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"error": "cannot create page",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if c.SaveUploadedFile(file, path.Join(notebook.storage, unique, binary)) != nil {
|
||||
notebook.delete(unique)
|
||||
c.HTML(500, "error.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"error": "cannot save binary",
|
||||
})
|
||||
} else {
|
||||
c.Redirect(302, webroot+"view/"+unique)
|
||||
}
|
||||
}
|
||||
})
|
||||
root.GET("/delete/:unique", func(c *gin.Context) {
|
||||
unique := c.Param("unique")
|
||||
if !IsValidNonce(unique) || !notebook.delete(unique) {
|
||||
c.HTML(404, "error.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"error": "cannot find page",
|
||||
})
|
||||
} else {
|
||||
c.Redirect(302, webroot)
|
||||
}
|
||||
})
|
||||
// Show edit page form (existing page).
|
||||
root.GET("/edit/:unique", func(c *gin.Context) {
|
||||
unique := c.Param("unique")
|
||||
page := notebook.get(unique)
|
||||
if page == nil {
|
||||
c.HTML(404, "error.tmpl", gin.H{
|
||||
if !IsValidNonce(unique, PageNonceSize) {
|
||||
c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"error": "cannot find page",
|
||||
})
|
||||
} else {
|
||||
c.HTML(200, "page-new.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"title": page["title"],
|
||||
"unique": unique,
|
||||
"error": "invalid page identifier",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
page, err := catalog.GetPage(unique)
|
||||
if err != nil || page == nil {
|
||||
c.HTML(http.StatusNotFound, "error.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"error": "page not found",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.HTML(200, "page-new.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"page": gin.H{"title": page.Title, "filename": page.Filename},
|
||||
"unique": unique,
|
||||
"title": page.Title,
|
||||
})
|
||||
})
|
||||
|
||||
// Create or update a page (handles file upload for new pages).
|
||||
root.POST("/edit", func(c *gin.Context) {
|
||||
unique := c.PostForm("unique")
|
||||
title := c.PostForm("title")
|
||||
|
||||
if len(title) < 1 {
|
||||
c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"error": "Title is required",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var uploadName string
|
||||
var uploadData []byte
|
||||
file, header, err := c.Request.FormFile("binary")
|
||||
if err == nil {
|
||||
defer file.Close()
|
||||
data, readErr := io.ReadAll(file)
|
||||
if readErr != nil {
|
||||
c.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"error": "Failed to read uploaded file",
|
||||
})
|
||||
return
|
||||
}
|
||||
uploadName = header.Filename
|
||||
uploadData = data
|
||||
} else if !errors.Is(err, http.ErrMissingFile) {
|
||||
c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"error": "Failed to read uploaded file",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if len(unique) > 0 {
|
||||
// Editing an existing page: rename and optionally replace the binary.
|
||||
if !IsValidNonce(unique, PageNonceSize) {
|
||||
c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"error": "invalid page identifier",
|
||||
})
|
||||
return
|
||||
}
|
||||
if err := catalog.RenamePage(unique, title); err != nil {
|
||||
c.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"error": "Failed to update page",
|
||||
})
|
||||
return
|
||||
}
|
||||
if len(uploadData) > 0 {
|
||||
if err := catalog.AttachBinary(unique, uploadName, uploadData); err != nil {
|
||||
c.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"error": "Failed to save binary file",
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
c.Redirect(http.StatusFound, webroot+"view/"+unique)
|
||||
return
|
||||
}
|
||||
|
||||
// Creating a new page: uploaded binary is optional.
|
||||
pageID, createErr := catalog.CreatePage(title, uploadName, uploadData)
|
||||
if createErr != nil {
|
||||
c.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"error": "Failed to create page",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusFound, webroot+"view/"+pageID)
|
||||
})
|
||||
|
||||
// View a page with all its cells.
|
||||
root.GET("/view/:unique", func(c *gin.Context) {
|
||||
unique := c.Param("unique")
|
||||
page := notebook.get(unique)
|
||||
if page == nil {
|
||||
c.HTML(404, "error.tmpl", gin.H{
|
||||
if !IsValidNonce(unique, PageNonceSize) {
|
||||
c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"error": "cannot find a new page",
|
||||
})
|
||||
} else {
|
||||
c.HTML(200, "page-view.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"page": page,
|
||||
"pipe": notebook.open(unique, false) != nil,
|
||||
"error": "invalid page identifier",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
page, err := catalog.GetPage(unique)
|
||||
if err != nil || page == nil {
|
||||
c.HTML(http.StatusNotFound, "error.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"error": "page not found",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Check if a Rizin pipe is open for this page.
|
||||
pipe := notebook.open(unique, false) != nil
|
||||
|
||||
// Load cells from SQLite.
|
||||
cells, _ := catalog.ListCells(unique)
|
||||
lines := make([]interface{}, len(cells))
|
||||
for i, cell := range cells {
|
||||
statsSource := cell.Content
|
||||
if cell.Type == "command" || cell.Type == "script" {
|
||||
statsSource = string(cell.Output)
|
||||
}
|
||||
lineCount := 0
|
||||
wordCount := 0
|
||||
if statsSource != "" {
|
||||
lineCount = strings.Count(statsSource, "\n") + 1
|
||||
wordCount = len(strings.Fields(statsSource))
|
||||
}
|
||||
actionTime := formatActionTime(cell.Created)
|
||||
if cell.Executed > 0 {
|
||||
actionTime = formatActionTime(cell.Executed)
|
||||
}
|
||||
lines[i] = gin.H{
|
||||
"type": cell.Type,
|
||||
"unique": cell.ID,
|
||||
"command": cell.Content,
|
||||
"script": cell.Content,
|
||||
"lineCount": lineCount,
|
||||
"wordCount": wordCount,
|
||||
"actionTime": actionTime,
|
||||
}
|
||||
}
|
||||
|
||||
pageData := gin.H{
|
||||
"title": page.Title,
|
||||
"unique": page.ID,
|
||||
"filename": page.Filename,
|
||||
"binary": page.Binary,
|
||||
"binaryHash": page.BinaryHash,
|
||||
"hasBinary": page.Binary != "",
|
||||
"lines": lines,
|
||||
}
|
||||
|
||||
c.HTML(200, "page-view.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"page": pageData,
|
||||
"pipe": pipe,
|
||||
"cmds": notebook.cmds,
|
||||
})
|
||||
})
|
||||
|
||||
// Delete a page.
|
||||
root.GET("/delete/:unique", func(c *gin.Context) {
|
||||
unique := c.Param("unique")
|
||||
if !IsValidNonce(unique, PageNonceSize) {
|
||||
c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"error": "invalid page identifier",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
notebook.closePipe(unique)
|
||||
catalog.DeletePage(unique)
|
||||
c.Redirect(http.StatusFound, webroot)
|
||||
})
|
||||
}
|
||||
@@ -1,40 +1,65 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func serverAddPipe(pipe *gin.RouterGroup) {
|
||||
|
||||
// Open a Rizin pipe for a page.
|
||||
pipe.GET("/open/:unique", func(c *gin.Context) {
|
||||
unique := c.Param("unique")
|
||||
if !IsValidNonce(unique) {
|
||||
c.HTML(404, "error.tmpl", gin.H{
|
||||
if !IsValidNonce(unique, PageNonceSize) {
|
||||
c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"error": "unknown page.",
|
||||
"error": "invalid page identifier",
|
||||
})
|
||||
return
|
||||
}
|
||||
rizin := notebook.open(unique, true)
|
||||
if rizin == nil {
|
||||
c.HTML(404, "error.tmpl", gin.H{
|
||||
|
||||
page, err := catalog.GetPage(unique)
|
||||
if err != nil || page == nil {
|
||||
c.HTML(http.StatusNotFound, "error.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"error": "failed to open the rizin pipe.",
|
||||
"error": "page not found",
|
||||
})
|
||||
} else {
|
||||
c.Redirect(302, webroot+"view/"+unique)
|
||||
return
|
||||
}
|
||||
|
||||
if page.Binary == "" {
|
||||
c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"error": "This page has no binary attached yet. Attach one from the edit page before opening a Rizin pipe.",
|
||||
"location": webroot + "edit/" + unique,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if notebook.open(unique, true) == nil {
|
||||
c.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"error": "Failed to open Rizin pipe. Check that the binary file exists and rizin is installed.",
|
||||
"location": webroot + "view/" + unique,
|
||||
})
|
||||
return
|
||||
}
|
||||
c.Redirect(http.StatusFound, webroot+"view/"+unique)
|
||||
})
|
||||
|
||||
// Close a Rizin pipe for a page.
|
||||
pipe.GET("/close/:unique", func(c *gin.Context) {
|
||||
unique := c.Param("unique")
|
||||
if !IsValidNonce(unique) {
|
||||
c.HTML(404, "error.tmpl", gin.H{
|
||||
if !IsValidNonce(unique, PageNonceSize) {
|
||||
c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"error": "unknown page.",
|
||||
"error": "invalid page identifier",
|
||||
})
|
||||
return
|
||||
} else {
|
||||
notebook.close(unique)
|
||||
c.Redirect(302, webroot+"view/"+unique)
|
||||
}
|
||||
|
||||
notebook.closePipe(unique)
|
||||
c.Redirect(http.StatusFound, webroot+"view/"+unique)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func apiJSONSearchPage(c *gin.Context) {
|
||||
pageID := c.Param("id")
|
||||
if !IsValidNonce(pageID, PageNonceSize) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid page identifier"})
|
||||
return
|
||||
}
|
||||
|
||||
page, err := catalog.GetPage(pageID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if page == nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "page not found"})
|
||||
return
|
||||
}
|
||||
|
||||
query := strings.TrimSpace(c.Query("query"))
|
||||
if query == "" {
|
||||
query = strings.TrimSpace(c.Query("q"))
|
||||
}
|
||||
|
||||
limit, err := parseNotebookSearchLimit(c.Query("limit"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
cells, err := catalog.ListCells(pageID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
response, err := SearchNotebook(cells, NotebookSearchOptions{
|
||||
Query: query,
|
||||
Mode: NotebookSearchMode(strings.TrimSpace(c.DefaultQuery("mode", string(NotebookSearchModeLiteral)))),
|
||||
CaseSensitive: parseNotebookSearchBool(c.Query("case_sensitive")),
|
||||
Limit: limit,
|
||||
SurfaceFilter: NotebookSearchSurfaceFilter(strings.TrimSpace(c.DefaultQuery("surface", string(NotebookSearchSurfaceFilterAll)))),
|
||||
CellTypeFilter: NotebookSearchCellTypeFilter(strings.TrimSpace(c.DefaultQuery("cell_type", string(NotebookSearchCellTypeFilterAll)))),
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
func parseNotebookSearchLimit(raw string) (int, error) {
|
||||
if strings.TrimSpace(raw) == "" {
|
||||
return getNotebookSearchConfig().MaxResults, nil
|
||||
}
|
||||
limit, err := strconv.Atoi(raw)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("invalid limit: %w", err)
|
||||
}
|
||||
if limit <= 0 {
|
||||
return getNotebookSearchConfig().MaxResults, nil
|
||||
}
|
||||
return limit, nil
|
||||
}
|
||||
|
||||
func parseNotebookSearchBool(raw string) bool {
|
||||
switch strings.ToLower(strings.TrimSpace(raw)) {
|
||||
case "1", "true", "yes", "on":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -1,77 +1,294 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func serverAddSettings(root *gin.RouterGroup) {
|
||||
root.GET("/settings", func(c *gin.Context) {
|
||||
c.HTML(200, "settings.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"environment": config.Environment,
|
||||
})
|
||||
})
|
||||
root.GET("/settings/:action/:section/:editkey", func(c *gin.Context) {
|
||||
action := c.Param("action")
|
||||
section := c.Param("section")
|
||||
editkey := c.Param("editkey")
|
||||
if action != "environment" {
|
||||
c.HTML(404, "error.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"error": "cannot find page",
|
||||
})
|
||||
return
|
||||
}
|
||||
if editkey == "new" {
|
||||
editkey = ""
|
||||
}
|
||||
var data map[string]string = nil
|
||||
if action == "environment" {
|
||||
data = config.Environment
|
||||
}
|
||||
c.HTML(200, "settings-edit.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"action": action,
|
||||
"section": section,
|
||||
"editkey": editkey,
|
||||
"data": data,
|
||||
})
|
||||
})
|
||||
root.POST("/settings", func(c *gin.Context) {
|
||||
key := c.DefaultPostForm("key", "")
|
||||
value := c.DefaultPostForm("value", "")
|
||||
action := c.DefaultPostForm("action", "")
|
||||
|
||||
if action != "environment" {
|
||||
c.HTML(404, "error.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"location": webroot + "settings",
|
||||
"error": "invalid settings (Action)",
|
||||
})
|
||||
return
|
||||
} else if action == "environment" {
|
||||
editkey := c.DefaultPostForm("editkey", "")
|
||||
subaction := c.DefaultPostForm("subaction", "")
|
||||
editkey = strings.TrimSpace(editkey)
|
||||
if editkey == "" && subaction != "new" {
|
||||
c.HTML(404, "error.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"location": webroot + "settings",
|
||||
"error": "invalid settings (Environment Variable)",
|
||||
})
|
||||
} else if subaction == "new" {
|
||||
config.SetEnvironment(key, value)
|
||||
} else if subaction == "delete" {
|
||||
config.DelEnvironment(editkey)
|
||||
} else {
|
||||
if editkey != key {
|
||||
config.DelEnvironment(editkey)
|
||||
}
|
||||
config.SetEnvironment(key, value)
|
||||
// View current settings.
|
||||
root.GET("/settings", func(c *gin.Context) {
|
||||
settings, _ := catalog.GetAllSettings()
|
||||
if settings == nil {
|
||||
settings = map[string]string{}
|
||||
}
|
||||
// Filter out structured settings from the environment display.
|
||||
env := map[string]string{}
|
||||
for k, v := range settings {
|
||||
if !strings.HasPrefix(k, keybindingSettingsPrefix) &&
|
||||
!strings.HasPrefix(k, autocompleteSettingsPrefix) &&
|
||||
!strings.HasPrefix(k, notebookSearchSettingsPrefix) {
|
||||
env[k] = v
|
||||
}
|
||||
}
|
||||
config.Save()
|
||||
c.Redirect(302, webroot+"settings")
|
||||
c.HTML(200, "settings.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"environment": env,
|
||||
"kb_actions": keybindingActions(),
|
||||
"ac_actions": autocompleteActions(),
|
||||
"ns_actions": notebookSearchActions(),
|
||||
})
|
||||
})
|
||||
|
||||
// Edit or create an environment variable form.
|
||||
root.GET("/settings/environment/edit/:key", func(c *gin.Context) {
|
||||
key := c.Param("key")
|
||||
|
||||
// "new" is a special sentinel value meaning create a new variable.
|
||||
editkey := ""
|
||||
if key != "new" {
|
||||
editkey = key
|
||||
}
|
||||
|
||||
settings, _ := catalog.GetAllSettings()
|
||||
if settings == nil {
|
||||
settings = map[string]string{}
|
||||
}
|
||||
|
||||
c.HTML(200, "settings-edit.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"action": "environment",
|
||||
"editkey": editkey,
|
||||
"data": settings,
|
||||
})
|
||||
})
|
||||
|
||||
// Edit a keybinding.
|
||||
root.GET("/settings/keybinding/edit/:action", func(c *gin.Context) {
|
||||
action := c.Param("action")
|
||||
if _, ok := DefaultKeybindings[action]; !ok {
|
||||
c.Redirect(http.StatusFound, webroot+"settings")
|
||||
return
|
||||
}
|
||||
kb := GetAllKeybindings()
|
||||
c.HTML(200, "settings-edit.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"action": "keybinding",
|
||||
"editkey": action,
|
||||
"label": KeybindingLabels[action],
|
||||
"combo": kb[action],
|
||||
"default_combo": DefaultKeybindings[action],
|
||||
})
|
||||
})
|
||||
|
||||
// Reset all keybindings to defaults.
|
||||
root.GET("/settings/keybinding/reset", func(c *gin.Context) {
|
||||
for action := range DefaultKeybindings {
|
||||
catalog.DeleteSetting(keybindingSettingsPrefix + action)
|
||||
}
|
||||
c.Redirect(http.StatusFound, webroot+"settings")
|
||||
})
|
||||
|
||||
// Edit an autocomplete setting.
|
||||
root.GET("/settings/autocomplete/edit/:key", func(c *gin.Context) {
|
||||
key := c.Param("key")
|
||||
if _, ok := AutocompleteLabels[key]; !ok {
|
||||
c.Redirect(http.StatusFound, webroot+"settings")
|
||||
return
|
||||
}
|
||||
cfg := GetAutocompleteConfig()
|
||||
c.HTML(200, "settings-edit.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"action": "autocomplete",
|
||||
"editkey": key,
|
||||
"label": AutocompleteLabels[key],
|
||||
"current_value": strconv.Itoa(cfg[key]),
|
||||
"default_value": AutocompleteDefaults[key],
|
||||
})
|
||||
})
|
||||
|
||||
// Reset all autocomplete settings to defaults.
|
||||
root.GET("/settings/autocomplete/reset", func(c *gin.Context) {
|
||||
for _, key := range AutocompleteSettingKeys {
|
||||
catalog.DeleteSetting(autocompleteSettingsPrefix + key)
|
||||
}
|
||||
c.Redirect(http.StatusFound, webroot+"settings")
|
||||
})
|
||||
|
||||
root.GET("/settings/search/edit/:key", func(c *gin.Context) {
|
||||
key := c.Param("key")
|
||||
if _, ok := notebookSearchSettingLabels[key]; !ok {
|
||||
c.Redirect(http.StatusFound, webroot+"settings")
|
||||
return
|
||||
}
|
||||
|
||||
cfg := getNotebookSearchConfig()
|
||||
currentValue := notebookSearchSettingDefaults[key]
|
||||
switch key {
|
||||
case "max_results":
|
||||
currentValue = strconv.Itoa(cfg.MaxResults)
|
||||
case "default_mode":
|
||||
currentValue = string(cfg.DefaultMode)
|
||||
case "default_surface":
|
||||
currentValue = string(cfg.Surface)
|
||||
case "default_cell_type":
|
||||
currentValue = string(cfg.CellType)
|
||||
}
|
||||
|
||||
c.HTML(200, "settings-edit.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"action": "search",
|
||||
"editkey": key,
|
||||
"label": notebookSearchSettingLabels[key],
|
||||
"current_value": currentValue,
|
||||
"default_value": notebookSearchSettingDefaults[key],
|
||||
"default_display": notebookSearchSettingValueLabel(key, notebookSearchSettingDefaults[key]),
|
||||
"input_type": notebookSearchSettingKinds[key],
|
||||
"options": notebookSearchSettingChoices[key],
|
||||
"min_value": strconv.Itoa(minSearchMaxResults),
|
||||
"max_value": strconv.Itoa(maxSearchMaxResults),
|
||||
})
|
||||
})
|
||||
|
||||
root.GET("/settings/search/reset", func(c *gin.Context) {
|
||||
for _, key := range notebookSearchSettingOrder {
|
||||
catalog.DeleteSetting(notebookSearchSettingsPrefix + key)
|
||||
}
|
||||
c.Redirect(http.StatusFound, webroot+"settings")
|
||||
})
|
||||
|
||||
// Handle settings form submissions.
|
||||
root.POST("/settings", func(c *gin.Context) {
|
||||
action := strings.TrimSpace(c.PostForm("action"))
|
||||
|
||||
if action == "environment" {
|
||||
handleEnvironmentSettings(c)
|
||||
return
|
||||
}
|
||||
|
||||
if action == "keybinding" {
|
||||
handleKeybindingSettings(c)
|
||||
return
|
||||
}
|
||||
|
||||
if action == "autocomplete" {
|
||||
handleAutocompleteSettings(c)
|
||||
return
|
||||
}
|
||||
|
||||
if action == "search" {
|
||||
handleNotebookSearchSettings(c)
|
||||
return
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusFound, webroot+"settings")
|
||||
})
|
||||
}
|
||||
|
||||
func handleEnvironmentSettings(c *gin.Context) {
|
||||
subaction := strings.TrimSpace(c.PostForm("subaction"))
|
||||
editkey := strings.TrimSpace(c.PostForm("editkey"))
|
||||
key := strings.TrimSpace(c.PostForm("key"))
|
||||
value := strings.TrimSpace(c.PostForm("value"))
|
||||
|
||||
switch subaction {
|
||||
case "new":
|
||||
if len(key) > 0 {
|
||||
catalog.SetSetting(key, value)
|
||||
os.Setenv(key, value)
|
||||
}
|
||||
case "edit":
|
||||
if len(editkey) > 0 && len(key) > 0 {
|
||||
if editkey != key {
|
||||
catalog.DeleteSetting(editkey)
|
||||
os.Unsetenv(editkey)
|
||||
}
|
||||
catalog.SetSetting(key, value)
|
||||
os.Setenv(key, value)
|
||||
}
|
||||
case "delete":
|
||||
if len(editkey) > 0 {
|
||||
catalog.DeleteSetting(editkey)
|
||||
os.Unsetenv(editkey)
|
||||
}
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusFound, webroot+"settings")
|
||||
}
|
||||
|
||||
func handleKeybindingSettings(c *gin.Context) {
|
||||
subaction := strings.TrimSpace(c.PostForm("subaction"))
|
||||
editkey := strings.TrimSpace(c.PostForm("editkey"))
|
||||
value := strings.TrimSpace(c.PostForm("value"))
|
||||
|
||||
switch subaction {
|
||||
case "save":
|
||||
if len(editkey) > 0 && len(value) > 0 {
|
||||
catalog.SetSetting(keybindingSettingsPrefix+editkey, value)
|
||||
}
|
||||
case "reset":
|
||||
if len(editkey) > 0 {
|
||||
catalog.DeleteSetting(keybindingSettingsPrefix + editkey)
|
||||
}
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusFound, webroot+"settings")
|
||||
}
|
||||
|
||||
func handleAutocompleteSettings(c *gin.Context) {
|
||||
subaction := strings.TrimSpace(c.PostForm("subaction"))
|
||||
editkey := strings.TrimSpace(c.PostForm("editkey"))
|
||||
value := strings.TrimSpace(c.PostForm("value"))
|
||||
|
||||
switch subaction {
|
||||
case "save":
|
||||
if len(editkey) > 0 && len(value) > 0 {
|
||||
// Validate and clamp the numeric value.
|
||||
val, err := strconv.Atoi(value)
|
||||
if err == nil {
|
||||
val = clampInt(val, minACMaxResults, maxACMaxResults)
|
||||
catalog.SetSetting(autocompleteSettingsPrefix+editkey, strconv.Itoa(val))
|
||||
}
|
||||
}
|
||||
case "reset":
|
||||
if len(editkey) > 0 {
|
||||
catalog.DeleteSetting(autocompleteSettingsPrefix + editkey)
|
||||
}
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusFound, webroot+"settings")
|
||||
}
|
||||
|
||||
func handleNotebookSearchSettings(c *gin.Context) {
|
||||
subaction := strings.TrimSpace(c.PostForm("subaction"))
|
||||
editkey := strings.TrimSpace(c.PostForm("editkey"))
|
||||
value := strings.TrimSpace(c.PostForm("value"))
|
||||
|
||||
switch subaction {
|
||||
case "save":
|
||||
switch editkey {
|
||||
case "max_results":
|
||||
val, err := strconv.Atoi(value)
|
||||
if err == nil {
|
||||
val = clampInt(val, minSearchMaxResults, maxSearchMaxResults)
|
||||
catalog.SetSetting(notebookSearchSettingsPrefix+editkey, strconv.Itoa(val))
|
||||
}
|
||||
case "default_mode":
|
||||
mode, err := normalizeNotebookSearchMode(NotebookSearchMode(value))
|
||||
if err == nil {
|
||||
catalog.SetSetting(notebookSearchSettingsPrefix+editkey, string(mode))
|
||||
}
|
||||
case "default_surface":
|
||||
surface, err := normalizeNotebookSearchSurfaceFilter(NotebookSearchSurfaceFilter(value))
|
||||
if err == nil {
|
||||
catalog.SetSetting(notebookSearchSettingsPrefix+editkey, string(surface))
|
||||
}
|
||||
case "default_cell_type":
|
||||
cellType, err := normalizeNotebookSearchCellTypeFilter(NotebookSearchCellTypeFilter(value))
|
||||
if err == nil {
|
||||
catalog.SetSetting(notebookSearchSettingsPrefix+editkey, string(cellType))
|
||||
}
|
||||
}
|
||||
case "reset":
|
||||
if len(editkey) > 0 {
|
||||
catalog.DeleteSetting(notebookSearchSettingsPrefix + editkey)
|
||||
}
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusFound, webroot+"settings")
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,43 @@
|
||||
// Copyright 2024 ByteDance Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package dirtmake
|
||||
|
||||
import (
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
type slice struct {
|
||||
data unsafe.Pointer
|
||||
len int
|
||||
cap int
|
||||
}
|
||||
|
||||
//go:linkname mallocgc runtime.mallocgc
|
||||
func mallocgc(size uintptr, typ unsafe.Pointer, needzero bool) unsafe.Pointer
|
||||
|
||||
// Bytes allocates a byte slice but does not clean up the memory it references.
|
||||
// Throw a fatal error instead of panic if cap is greater than runtime.maxAlloc.
|
||||
// NOTE: MUST set any byte element before it's read.
|
||||
func Bytes(len, cap int) (b []byte) {
|
||||
if len < 0 || len > cap {
|
||||
panic("dirtmake.Bytes: len out of range")
|
||||
}
|
||||
p := mallocgc(uintptr(cap), nil, false)
|
||||
sh := (*slice)(unsafe.Pointer(&b))
|
||||
sh.data = p
|
||||
sh.len = len
|
||||
sh.cap = cap
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
[codespell]
|
||||
# ignore test files, go project names, binary files via `skip` and special var/regex via `ignore-words`
|
||||
skip = fuzz,*_test.tmpl,testdata,*_test.go,go.mod,go.sum,*.gz
|
||||
ignore-words = .github/workflows/.ignore_words
|
||||
check-filenames = true
|
||||
@@ -0,0 +1,55 @@
|
||||
*.o
|
||||
*.swp
|
||||
*.swm
|
||||
*.swn
|
||||
*.a
|
||||
*.so
|
||||
_obj
|
||||
_test
|
||||
*.[568vq]
|
||||
[568vq].out
|
||||
*.cgo1.go
|
||||
*.cgo2.c
|
||||
_cgo_defun.c
|
||||
_cgo_gotypes.go
|
||||
_cgo_export.*
|
||||
_testmain.go
|
||||
*.exe
|
||||
*.exe~
|
||||
*.test
|
||||
*.prof
|
||||
*.rar
|
||||
*.zip
|
||||
*.gz
|
||||
*.psd
|
||||
*.bmd
|
||||
*.cfg
|
||||
*.pptx
|
||||
*.log
|
||||
*nohup.out
|
||||
*settings.pyc
|
||||
*.sublime-project
|
||||
*.sublime-workspace
|
||||
.DS_Store
|
||||
/.idea/
|
||||
/.vscode/
|
||||
/output/
|
||||
/vendor/
|
||||
/Gopkg.lock
|
||||
/Gopkg.toml
|
||||
coverage.html
|
||||
coverage.out
|
||||
coverage.xml
|
||||
junit.xml
|
||||
*.profile
|
||||
*.svg
|
||||
*.out
|
||||
ast/test.out
|
||||
ast/bench.sh
|
||||
|
||||
!testdata/**/*.json.gz
|
||||
fuzz/testdata
|
||||
*__debug_bin*
|
||||
*pprof
|
||||
*coverage.txt
|
||||
tools/venv/*
|
||||
@@ -0,0 +1,9 @@
|
||||
[submodule "cloudwego"]
|
||||
path = tools/asm2asm
|
||||
url = https://github.com/cloudwego/asm2asm.git
|
||||
[submodule "tools/simde"]
|
||||
path = tools/simde
|
||||
url = https://github.com/simd-everywhere/simde.git
|
||||
[submodule "fuzz/go-fuzz-corpus"]
|
||||
path = fuzz/go-fuzz-corpus
|
||||
url = https://github.com/dvyukov/go-fuzz-corpus.git
|
||||
@@ -0,0 +1,24 @@
|
||||
header:
|
||||
license:
|
||||
spdx-id: Apache-2.0
|
||||
copyright-owner: ByteDance Inc.
|
||||
|
||||
paths:
|
||||
- '**/*.go'
|
||||
- '**/*.s'
|
||||
|
||||
paths-ignore:
|
||||
- 'ast/asm.s' # empty file
|
||||
- 'decoder/asm.s' # empty file
|
||||
- 'encoder/asm.s' # empty file
|
||||
- 'internal/caching/asm.s' # empty file
|
||||
- 'internal/jit/asm.s' # empty file
|
||||
- 'internal/native/avx/native_amd64.s' # auto-generated by asm2asm
|
||||
- 'internal/native/avx/native_subr_amd64.go' # auto-generated by asm2asm
|
||||
- 'internal/native/avx2/native_amd64.s' # auto-generated by asm2asm
|
||||
- 'internal/native/avx2/native_subr_amd64.go' # auto-generated by asm2asm
|
||||
- 'internal/resolver/asm.s' # empty file
|
||||
- 'internal/rt/asm.s' # empty file
|
||||
- 'internal/loader/asm.s' # empty file
|
||||
|
||||
comment: on-failure
|
||||
@@ -0,0 +1,128 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
wudi.daniel@bytedance.com.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||
enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
||||
@@ -0,0 +1,63 @@
|
||||
# How to Contribute
|
||||
|
||||
## Your First Pull Request
|
||||
We use GitHub for our codebase. You can start by reading [How To Pull Request](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests).
|
||||
|
||||
## Without Semantic Versioning
|
||||
We keep the stable code in branch `main` like `golang.org/x`. Development base on branch `develop`. We promise the **Forward Compatibility** by adding new package directory with suffix `v2/v3` when code has break changes.
|
||||
|
||||
## Branch Organization
|
||||
We use [git-flow](https://nvie.com/posts/a-successful-git-branching-model/) as our branch organization, as known as [FDD](https://en.wikipedia.org/wiki/Feature-driven_development)
|
||||
|
||||
|
||||
## Bugs
|
||||
### 1. How to Find Known Issues
|
||||
We are using [Github Issues](https://github.com/bytedance/sonic/issues) for our public bugs. We keep a close eye on this and try to make it clear when we have an internal fix in progress. Before filing a new task, try to make sure your problem doesn’t already exist.
|
||||
|
||||
### 2. Reporting New Issues
|
||||
Providing a reduced test code is a recommended way for reporting issues. Then can be placed in:
|
||||
- Just in issues
|
||||
- [Golang Playground](https://play.golang.org/)
|
||||
|
||||
### 3. Security Bugs
|
||||
Please do not report the safe disclosure of bugs to public issues. Contact us by [Support Email](mailto:[email protected])
|
||||
|
||||
## How to Get in Touch
|
||||
- [Email](mailto:[email protected])
|
||||
|
||||
## Submit a Pull Request
|
||||
Before you submit your Pull Request (PR) consider the following guidelines:
|
||||
1. Search [GitHub](https://github.com/bytedance/sonic/pulls) for an open or closed PR that relates to your submission. You don't want to duplicate existing efforts.
|
||||
2. Be sure that an issue describes the problem you're fixing, or documents the design for the feature you'd like to add. Discussing the design upfront helps to ensure that we're ready to accept your work.
|
||||
3. [Fork](https://docs.github.com/en/github/getting-started-with-github/fork-a-repo) the bytedance/sonic repo.
|
||||
4. In your forked repository, make your changes in a new git branch:
|
||||
```
|
||||
git checkout -b bugfix/security_bug develop
|
||||
```
|
||||
5. Create your patch, including appropriate test cases.
|
||||
6. Follow our [Style Guides](#code-style-guides).
|
||||
7. Commit your changes using a descriptive commit message that follows [AngularJS Git Commit Message Conventions](https://docs.google.com/document/d/1QrDFcIiPjSLDn3EL15IJygNPiHORgU1_OOAqWjiDU5Y/edit).
|
||||
Adherence to these conventions is necessary because release notes will be automatically generated from these messages.
|
||||
8. Push your branch to GitHub:
|
||||
```
|
||||
git push origin bugfix/security_bug
|
||||
```
|
||||
9. In GitHub, send a pull request to `sonic:main`
|
||||
|
||||
Note: you must use one of `optimize/feature/bugfix/doc/ci/test/refactor` following a slash(`/`) as the branch prefix.
|
||||
|
||||
Your pr title and commit message should follow https://www.conventionalcommits.org/.
|
||||
|
||||
## Contribution Prerequisites
|
||||
- Our development environment keeps up with [Go Official](https://golang.org/project/).
|
||||
- You need fully checking with lint tools before submit your pull request. [gofmt](https://golang.org/pkg/cmd/gofmt/) & [golangci-lint](https://github.com/golangci/golangci-lint)
|
||||
- You are familiar with [Github](https://github.com)
|
||||
- Maybe you need familiar with [Actions](https://github.com/features/actions)(our default workflow tool).
|
||||
|
||||
## Code Style Guides
|
||||
See [Go Code Review Comments](https://github.com/golang/go/wiki/CodeReviewComments).
|
||||
|
||||
Good resources:
|
||||
- [Effective Go](https://golang.org/doc/effective_go)
|
||||
- [Pingcap General advice](https://pingcap.github.io/style-guide/general.html)
|
||||
- [Uber Go Style Guide](https://github.com/uber-go/guide/blob/master/style.md)
|
||||
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,501 @@
|
||||
# Sonic
|
||||
|
||||
English | [中文](README_ZH_CN.md)
|
||||
|
||||
A blazingly fast JSON serializing & deserializing library, accelerated by JIT (just-in-time compiling) and SIMD (single-instruction-multiple-data).
|
||||
|
||||
## Requirement
|
||||
|
||||
- Go: 1.18~1.25
|
||||
- Notice: Go1.24.0 is not supported due to the [issue](https://github.com/golang/go/issues/71672), please use higher go version or add build tag `--ldflags="-checklinkname=0"`
|
||||
- OS: Linux / MacOS / Windows
|
||||
- CPU: AMD64 / (ARM64, need go1.20 above)
|
||||
|
||||
## Features
|
||||
|
||||
- Runtime object binding without code generation
|
||||
- Complete APIs for JSON value manipulation
|
||||
- Fast, fast, fast!
|
||||
|
||||
## APIs
|
||||
|
||||
see [go.dev](https://pkg.go.dev/github.com/bytedance/sonic)
|
||||
|
||||
## Benchmarks
|
||||
|
||||
For **all sizes** of json and **all scenarios** of usage, **Sonic performs best**.
|
||||
|
||||
- [Medium](https://github.com/bytedance/sonic/blob/main/decoder/testdata_test.go#L19) (13KB, 300+ key, 6 layers)
|
||||
|
||||
```powershell
|
||||
goversion: 1.17.1
|
||||
goos: darwin
|
||||
goarch: amd64
|
||||
cpu: Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz
|
||||
BenchmarkEncoder_Generic_Sonic-16 32393 ns/op 402.40 MB/s 11965 B/op 4 allocs/op
|
||||
BenchmarkEncoder_Generic_Sonic_Fast-16 21668 ns/op 601.57 MB/s 10940 B/op 4 allocs/op
|
||||
BenchmarkEncoder_Generic_JsonIter-16 42168 ns/op 309.12 MB/s 14345 B/op 115 allocs/op
|
||||
BenchmarkEncoder_Generic_GoJson-16 65189 ns/op 199.96 MB/s 23261 B/op 16 allocs/op
|
||||
BenchmarkEncoder_Generic_StdLib-16 106322 ns/op 122.60 MB/s 49136 B/op 789 allocs/op
|
||||
BenchmarkEncoder_Binding_Sonic-16 6269 ns/op 2079.26 MB/s 14173 B/op 4 allocs/op
|
||||
BenchmarkEncoder_Binding_Sonic_Fast-16 5281 ns/op 2468.16 MB/s 12322 B/op 4 allocs/op
|
||||
BenchmarkEncoder_Binding_JsonIter-16 20056 ns/op 649.93 MB/s 9488 B/op 2 allocs/op
|
||||
BenchmarkEncoder_Binding_GoJson-16 8311 ns/op 1568.32 MB/s 9481 B/op 1 allocs/op
|
||||
BenchmarkEncoder_Binding_StdLib-16 16448 ns/op 792.52 MB/s 9479 B/op 1 allocs/op
|
||||
BenchmarkEncoder_Parallel_Generic_Sonic-16 6681 ns/op 1950.93 MB/s 12738 B/op 4 allocs/op
|
||||
BenchmarkEncoder_Parallel_Generic_Sonic_Fast-16 4179 ns/op 3118.99 MB/s 10757 B/op 4 allocs/op
|
||||
BenchmarkEncoder_Parallel_Generic_JsonIter-16 9861 ns/op 1321.84 MB/s 14362 B/op 115 allocs/op
|
||||
BenchmarkEncoder_Parallel_Generic_GoJson-16 18850 ns/op 691.52 MB/s 23278 B/op 16 allocs/op
|
||||
BenchmarkEncoder_Parallel_Generic_StdLib-16 45902 ns/op 283.97 MB/s 49174 B/op 789 allocs/op
|
||||
BenchmarkEncoder_Parallel_Binding_Sonic-16 1480 ns/op 8810.09 MB/s 13049 B/op 4 allocs/op
|
||||
BenchmarkEncoder_Parallel_Binding_Sonic_Fast-16 1209 ns/op 10785.23 MB/s 11546 B/op 4 allocs/op
|
||||
BenchmarkEncoder_Parallel_Binding_JsonIter-16 6170 ns/op 2112.58 MB/s 9504 B/op 2 allocs/op
|
||||
BenchmarkEncoder_Parallel_Binding_GoJson-16 3321 ns/op 3925.52 MB/s 9496 B/op 1 allocs/op
|
||||
BenchmarkEncoder_Parallel_Binding_StdLib-16 3739 ns/op 3486.49 MB/s 9480 B/op 1 allocs/op
|
||||
|
||||
BenchmarkDecoder_Generic_Sonic-16 66812 ns/op 195.10 MB/s 57602 B/op 723 allocs/op
|
||||
BenchmarkDecoder_Generic_Sonic_Fast-16 54523 ns/op 239.07 MB/s 49786 B/op 313 allocs/op
|
||||
BenchmarkDecoder_Generic_StdLib-16 124260 ns/op 104.90 MB/s 50869 B/op 772 allocs/op
|
||||
BenchmarkDecoder_Generic_JsonIter-16 91274 ns/op 142.81 MB/s 55782 B/op 1068 allocs/op
|
||||
BenchmarkDecoder_Generic_GoJson-16 88569 ns/op 147.17 MB/s 66367 B/op 973 allocs/op
|
||||
BenchmarkDecoder_Binding_Sonic-16 32557 ns/op 400.38 MB/s 28302 B/op 137 allocs/op
|
||||
BenchmarkDecoder_Binding_Sonic_Fast-16 28649 ns/op 455.00 MB/s 24999 B/op 34 allocs/op
|
||||
BenchmarkDecoder_Binding_StdLib-16 111437 ns/op 116.97 MB/s 10576 B/op 208 allocs/op
|
||||
BenchmarkDecoder_Binding_JsonIter-16 35090 ns/op 371.48 MB/s 14673 B/op 385 allocs/op
|
||||
BenchmarkDecoder_Binding_GoJson-16 28738 ns/op 453.59 MB/s 22039 B/op 49 allocs/op
|
||||
BenchmarkDecoder_Parallel_Generic_Sonic-16 12321 ns/op 1057.91 MB/s 57233 B/op 723 allocs/op
|
||||
BenchmarkDecoder_Parallel_Generic_Sonic_Fast-16 10644 ns/op 1224.64 MB/s 49362 B/op 313 allocs/op
|
||||
BenchmarkDecoder_Parallel_Generic_StdLib-16 57587 ns/op 226.35 MB/s 50874 B/op 772 allocs/op
|
||||
BenchmarkDecoder_Parallel_Generic_JsonIter-16 38666 ns/op 337.12 MB/s 55789 B/op 1068 allocs/op
|
||||
BenchmarkDecoder_Parallel_Generic_GoJson-16 30259 ns/op 430.79 MB/s 66370 B/op 974 allocs/op
|
||||
BenchmarkDecoder_Parallel_Binding_Sonic-16 5965 ns/op 2185.28 MB/s 27747 B/op 137 allocs/op
|
||||
BenchmarkDecoder_Parallel_Binding_Sonic_Fast-16 5170 ns/op 2521.31 MB/s 24715 B/op 34 allocs/op
|
||||
BenchmarkDecoder_Parallel_Binding_StdLib-16 27582 ns/op 472.58 MB/s 10576 B/op 208 allocs/op
|
||||
BenchmarkDecoder_Parallel_Binding_JsonIter-16 13571 ns/op 960.51 MB/s 14685 B/op 385 allocs/op
|
||||
BenchmarkDecoder_Parallel_Binding_GoJson-16 10031 ns/op 1299.51 MB/s 22111 B/op 49 allocs/op
|
||||
|
||||
BenchmarkGetOne_Sonic-16 3276 ns/op 3975.78 MB/s 24 B/op 1 allocs/op
|
||||
BenchmarkGetOne_Gjson-16 9431 ns/op 1380.81 MB/s 0 B/op 0 allocs/op
|
||||
BenchmarkGetOne_Jsoniter-16 51178 ns/op 254.46 MB/s 27936 B/op 647 allocs/op
|
||||
BenchmarkGetOne_Parallel_Sonic-16 216.7 ns/op 60098.95 MB/s 24 B/op 1 allocs/op
|
||||
BenchmarkGetOne_Parallel_Gjson-16 1076 ns/op 12098.62 MB/s 0 B/op 0 allocs/op
|
||||
BenchmarkGetOne_Parallel_Jsoniter-16 17741 ns/op 734.06 MB/s 27945 B/op 647 allocs/op
|
||||
BenchmarkSetOne_Sonic-16 9571 ns/op 1360.61 MB/s 1584 B/op 17 allocs/op
|
||||
BenchmarkSetOne_Sjson-16 36456 ns/op 357.22 MB/s 52180 B/op 9 allocs/op
|
||||
BenchmarkSetOne_Jsoniter-16 79475 ns/op 163.86 MB/s 45862 B/op 964 allocs/op
|
||||
BenchmarkSetOne_Parallel_Sonic-16 850.9 ns/op 15305.31 MB/s 1584 B/op 17 allocs/op
|
||||
BenchmarkSetOne_Parallel_Sjson-16 18194 ns/op 715.77 MB/s 52247 B/op 9 allocs/op
|
||||
BenchmarkSetOne_Parallel_Jsoniter-16 33560 ns/op 388.05 MB/s 45892 B/op 964 allocs/op
|
||||
BenchmarkLoadNode/LoadAll()-16 11384 ns/op 1143.93 MB/s 6307 B/op 25 allocs/op
|
||||
BenchmarkLoadNode_Parallel/LoadAll()-16 5493 ns/op 2370.68 MB/s 7145 B/op 25 allocs/op
|
||||
BenchmarkLoadNode/Interface()-16 17722 ns/op 734.85 MB/s 13323 B/op 88 allocs/op
|
||||
BenchmarkLoadNode_Parallel/Interface()-16 10330 ns/op 1260.70 MB/s 15178 B/op 88 allocs/op
|
||||
```
|
||||
|
||||
- [Small](https://github.com/bytedance/sonic/blob/main/testdata/small.go) (400B, 11 keys, 3 layers)
|
||||

|
||||
- [Large](https://github.com/bytedance/sonic/blob/main/testdata/twitter.json) (635KB, 10000+ key, 6 layers)
|
||||

|
||||
|
||||
See [bench.sh](https://github.com/bytedance/sonic/blob/main/scripts/bench.sh) for benchmark codes.
|
||||
|
||||
## How it works
|
||||
|
||||
See [INTRODUCTION.md](./docs/INTRODUCTION.md).
|
||||
|
||||
## Usage
|
||||
|
||||
### Marshal/Unmarshal
|
||||
|
||||
Default behaviors are mostly consistent with `encoding/json`, except HTML escaping form (see [Escape HTML](https://github.com/bytedance/sonic/blob/main/README.md#escape-html)) and `SortKeys` feature (optional support see [Sort Keys](https://github.com/bytedance/sonic/blob/main/README.md#sort-keys)) that is **NOT** in conformity to [RFC8259](https://datatracker.ietf.org/doc/html/rfc8259).
|
||||
|
||||
```go
|
||||
import "github.com/bytedance/sonic"
|
||||
|
||||
var data YourSchema
|
||||
// Marshal
|
||||
output, err := sonic.Marshal(&data)
|
||||
// Unmarshal
|
||||
err := sonic.Unmarshal(output, &data)
|
||||
```
|
||||
|
||||
### Streaming IO
|
||||
|
||||
Sonic supports decoding json from `io.Reader` or encoding objects into `io.Writer`, aims at handling multiple values as well as reducing memory consumption.
|
||||
|
||||
- encoder
|
||||
|
||||
```go
|
||||
var o1 = map[string]interface{}{
|
||||
"a": "b",
|
||||
}
|
||||
var o2 = 1
|
||||
var w = bytes.NewBuffer(nil)
|
||||
var enc = sonic.ConfigDefault.NewEncoder(w)
|
||||
enc.Encode(o1)
|
||||
enc.Encode(o2)
|
||||
fmt.Println(w.String())
|
||||
// Output:
|
||||
// {"a":"b"}
|
||||
// 1
|
||||
```
|
||||
|
||||
- decoder
|
||||
|
||||
```go
|
||||
var o = map[string]interface{}{}
|
||||
var r = strings.NewReader(`{"a":"b"}{"1":"2"}`)
|
||||
var dec = sonic.ConfigDefault.NewDecoder(r)
|
||||
dec.Decode(&o)
|
||||
dec.Decode(&o)
|
||||
fmt.Printf("%+v", o)
|
||||
// Output:
|
||||
// map[1:2 a:b]
|
||||
```
|
||||
|
||||
### Use Number/Use Int64
|
||||
|
||||
```go
|
||||
import "github.com/bytedance/sonic/decoder"
|
||||
|
||||
var input = `1`
|
||||
var data interface{}
|
||||
|
||||
// default float64
|
||||
dc := decoder.NewDecoder(input)
|
||||
dc.Decode(&data) // data == float64(1)
|
||||
// use json.Number
|
||||
dc = decoder.NewDecoder(input)
|
||||
dc.UseNumber()
|
||||
dc.Decode(&data) // data == json.Number("1")
|
||||
// use int64
|
||||
dc = decoder.NewDecoder(input)
|
||||
dc.UseInt64()
|
||||
dc.Decode(&data) // data == int64(1)
|
||||
|
||||
root, err := sonic.GetFromString(input)
|
||||
// Get json.Number
|
||||
jn := root.Number()
|
||||
jm := root.InterfaceUseNumber().(json.Number) // jn == jm
|
||||
// Get float64
|
||||
fn := root.Float64()
|
||||
fm := root.Interface().(float64) // jn == jm
|
||||
```
|
||||
|
||||
### Sort Keys
|
||||
|
||||
On account of the performance loss from sorting (roughly 10%), sonic doesn't enable this feature by default. If your component depends on it to work (like [zstd](https://github.com/facebook/zstd)), Use it like this:
|
||||
|
||||
```go
|
||||
import "github.com/bytedance/sonic"
|
||||
import "github.com/bytedance/sonic/encoder"
|
||||
|
||||
// Binding map only
|
||||
m := map[string]interface{}{}
|
||||
v, err := encoder.Encode(m, encoder.SortMapKeys)
|
||||
|
||||
// Or ast.Node.SortKeys() before marshal
|
||||
var root := sonic.Get(JSON)
|
||||
err := root.SortKeys()
|
||||
```
|
||||
|
||||
### Escape HTML
|
||||
|
||||
On account of the performance loss (roughly 15%), sonic doesn't enable this feature by default. You can use `encoder.EscapeHTML` option to open this feature (align with `encoding/json.HTMLEscape`).
|
||||
|
||||
```go
|
||||
import "github.com/bytedance/sonic"
|
||||
|
||||
v := map[string]string{"&&":"<>"}
|
||||
ret, err := Encode(v, EscapeHTML) // ret == `{"\u0026\u0026":{"X":"\u003c\u003e"}}`
|
||||
```
|
||||
|
||||
### Compact Format
|
||||
|
||||
Sonic encodes primitive objects (struct/map...) as compact-format JSON by default, except marshaling `json.RawMessage` or `json.Marshaler`: sonic ensures validating their output JSON but **DO NOT** compacting them for performance concerns. We provide the option `encoder.CompactMarshaler` to add compacting process.
|
||||
|
||||
### Print Error
|
||||
|
||||
If there invalid syntax in input JSON, sonic will return `decoder.SyntaxError`, which supports pretty-printing of error position
|
||||
|
||||
```go
|
||||
import "github.com/bytedance/sonic"
|
||||
import "github.com/bytedance/sonic/decoder"
|
||||
|
||||
var data interface{}
|
||||
err := sonic.UnmarshalString("[[[}]]", &data)
|
||||
if err != nil {
|
||||
/* One line by default */
|
||||
println(e.Error()) // "Syntax error at index 3: invalid char\n\n\t[[[}]]\n\t...^..\n"
|
||||
/* Pretty print */
|
||||
if e, ok := err.(decoder.SyntaxError); ok {
|
||||
/*Syntax error at index 3: invalid char
|
||||
|
||||
[[[}]]
|
||||
...^..
|
||||
*/
|
||||
print(e.Description())
|
||||
} else if me, ok := err.(*decoder.MismatchTypeError); ok {
|
||||
// decoder.MismatchTypeError is new to Sonic v1.6.0
|
||||
print(me.Description())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Mismatched Types [Sonic v1.6.0]
|
||||
|
||||
If there a **mismatch-typed** value for a given key, sonic will report `decoder.MismatchTypeError` (if there are many, report the last one), but still skip wrong the value and keep decoding next JSON.
|
||||
|
||||
```go
|
||||
import "github.com/bytedance/sonic"
|
||||
import "github.com/bytedance/sonic/decoder"
|
||||
|
||||
var data = struct{
|
||||
A int
|
||||
B int
|
||||
}{}
|
||||
err := UnmarshalString(`{"A":"1","B":1}`, &data)
|
||||
println(err.Error()) // Mismatch type int with value string "at index 5: mismatched type with value\n\n\t{\"A\":\"1\",\"B\":1}\n\t.....^.........\n"
|
||||
fmt.Printf("%+v", data) // {A:0 B:1}
|
||||
```
|
||||
|
||||
### Ast.Node
|
||||
|
||||
Sonic/ast.Node is a completely self-contained AST for JSON. It implements serialization and deserialization both and provides robust APIs for obtaining and modification of generic data.
|
||||
|
||||
#### Get/Index
|
||||
|
||||
Search partial JSON by given paths, which must be non-negative integer or string, or nil
|
||||
|
||||
```go
|
||||
import "github.com/bytedance/sonic"
|
||||
|
||||
input := []byte(`{"key1":[{},{"key2":{"key3":[1,2,3]}}]}`)
|
||||
|
||||
// no path, returns entire json
|
||||
root, err := sonic.Get(input)
|
||||
raw := root.Raw() // == string(input)
|
||||
|
||||
// multiple paths
|
||||
root, err := sonic.Get(input, "key1", 1, "key2")
|
||||
sub := root.Get("key3").Index(2).Int64() // == 3
|
||||
```
|
||||
|
||||
**Tip**: since `Index()` uses offset to locate data, which is much faster than scanning like `Get()`, we suggest you use it as much as possible. And sonic also provides another API `IndexOrGet()` to underlying use offset as well as ensure the key is matched.
|
||||
|
||||
#### SearchOption
|
||||
|
||||
`Searcher` provides some options for user to meet different needs:
|
||||
|
||||
```go
|
||||
opts := ast.SearchOption{ CopyReturn: true ... }
|
||||
val, err := sonic.GetWithOptions(JSON, opts, "key")
|
||||
```
|
||||
|
||||
- CopyReturn
|
||||
Indicate the searcher to copy the result JSON string instead of refer from the input. This can help to reduce memory usage if you cache the results
|
||||
- ConcurentRead
|
||||
Since `ast.Node` use `Lazy-Load` design, it doesn't support Concurrently-Read by default. If you want to read it concurrently, please specify it.
|
||||
- ValidateJSON
|
||||
Indicate the searcher to validate the entire JSON. This option is enabled by default, which slow down the search speed a little.
|
||||
|
||||
#### Set/Unset
|
||||
|
||||
Modify the json content by Set()/Unset()
|
||||
|
||||
```go
|
||||
import "github.com/bytedance/sonic"
|
||||
|
||||
// Set
|
||||
exist, err := root.Set("key4", NewBool(true)) // exist == false
|
||||
alias1 := root.Get("key4")
|
||||
println(alias1.Valid()) // true
|
||||
alias2 := root.Index(1)
|
||||
println(alias1 == alias2) // true
|
||||
|
||||
// Unset
|
||||
exist, err := root.UnsetByIndex(1) // exist == true
|
||||
println(root.Get("key4").Check()) // "value not exist"
|
||||
```
|
||||
|
||||
#### Serialize
|
||||
|
||||
To encode `ast.Node` as json, use `MarshalJson()` or `json.Marshal()` (MUST pass the node's pointer)
|
||||
|
||||
```go
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/bytedance/sonic"
|
||||
)
|
||||
|
||||
buf, err := root.MarshalJson()
|
||||
println(string(buf)) // {"key1":[{},{"key2":{"key3":[1,2,3]}}]}
|
||||
exp, err := json.Marshal(&root) // WARN: use pointer
|
||||
println(string(buf) == string(exp)) // true
|
||||
```
|
||||
|
||||
#### APIs
|
||||
|
||||
- validation: `Check()`, `Error()`, `Valid()`, `Exist()`
|
||||
- searching: `Index()`, `Get()`, `IndexPair()`, `IndexOrGet()`, `GetByPath()`
|
||||
- go-type casting: `Int64()`, `Float64()`, `String()`, `Number()`, `Bool()`, `Map[UseNumber|UseNode]()`, `Array[UseNumber|UseNode]()`, `Interface[UseNumber|UseNode]()`
|
||||
- go-type packing: `NewRaw()`, `NewNumber()`, `NewNull()`, `NewBool()`, `NewString()`, `NewObject()`, `NewArray()`
|
||||
- iteration: `Values()`, `Properties()`, `ForEach()`, `SortKeys()`
|
||||
- modification: `Set()`, `SetByIndex()`, `Add()`
|
||||
|
||||
### Ast.Visitor
|
||||
|
||||
Sonic provides an advanced API for fully parsing JSON into non-standard types (neither `struct` not `map[string]interface{}`) without using any intermediate representation (`ast.Node` or `interface{}`). For example, you might have the following types which are like `interface{}` but actually not `interface{}`:
|
||||
|
||||
```go
|
||||
type UserNode interface {}
|
||||
|
||||
// the following types implement the UserNode interface.
|
||||
type (
|
||||
UserNull struct{}
|
||||
UserBool struct{ Value bool }
|
||||
UserInt64 struct{ Value int64 }
|
||||
UserFloat64 struct{ Value float64 }
|
||||
UserString struct{ Value string }
|
||||
UserObject struct{ Value map[string]UserNode }
|
||||
UserArray struct{ Value []UserNode }
|
||||
)
|
||||
```
|
||||
|
||||
Sonic provides the following API to return **the preorder traversal of a JSON AST**. The `ast.Visitor` is a SAX style interface which is used in some C++ JSON library. You should implement `ast.Visitor` by yourself and pass it to `ast.Preorder()` method. In your visitor you can make your custom types to represent JSON values. There may be an O(n) space container (such as stack) in your visitor to record the object / array hierarchy.
|
||||
|
||||
```go
|
||||
func Preorder(str string, visitor Visitor, opts *VisitorOptions) error
|
||||
|
||||
type Visitor interface {
|
||||
OnNull() error
|
||||
OnBool(v bool) error
|
||||
OnString(v string) error
|
||||
OnInt64(v int64, n json.Number) error
|
||||
OnFloat64(v float64, n json.Number) error
|
||||
OnObjectBegin(capacity int) error
|
||||
OnObjectKey(key string) error
|
||||
OnObjectEnd() error
|
||||
OnArrayBegin(capacity int) error
|
||||
OnArrayEnd() error
|
||||
}
|
||||
```
|
||||
|
||||
See [ast/visitor.go](https://github.com/bytedance/sonic/blob/main/ast/visitor.go) for detailed usage. We also implement a demo visitor for `UserNode` in [ast/visitor_test.go](https://github.com/bytedance/sonic/blob/main/ast/visitor_test.go).
|
||||
|
||||
## Compatibility
|
||||
|
||||
For developers who want to use sonic to meet different scenarios, we provide some integrated configs as `sonic.API`
|
||||
|
||||
- `ConfigDefault`: the sonic's default config (`EscapeHTML=false`,`SortKeys=false`...) to run sonic fast meanwhile ensure security.
|
||||
- `ConfigStd`: the std-compatible config (`EscapeHTML=true`,`SortKeys=true`...)
|
||||
- `ConfigFastest`: the fastest config (`NoQuoteTextMarshaler=true`) to run on sonic as fast as possible.
|
||||
Sonic **DOES NOT** ensure to support all environments, due to the difficulty of developing high-performance codes. On non-sonic-supporting environment, the implementation will fall back to `encoding/json`. Thus below configs will all equal to `ConfigStd`.
|
||||
|
||||
## Tips
|
||||
|
||||
### Pretouch
|
||||
|
||||
Since Sonic uses [golang-asm](https://github.com/twitchyliquid64/golang-asm) as a JIT assembler, which is NOT very suitable for runtime compiling, first-hit running of a huge schema may cause request-timeout or even process-OOM. For better stability, we advise **using `Pretouch()` for huge-schema or compact-memory applications** before `Marshal()/Unmarshal()`.
|
||||
|
||||
```go
|
||||
import (
|
||||
"reflect"
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/bytedance/sonic/option"
|
||||
)
|
||||
|
||||
func init() {
|
||||
var v HugeStruct
|
||||
|
||||
// For most large types (nesting depth <= option.DefaultMaxInlineDepth)
|
||||
err := sonic.Pretouch(reflect.TypeOf(v))
|
||||
|
||||
// with more CompileOption...
|
||||
err := sonic.Pretouch(reflect.TypeOf(v),
|
||||
// If the type is too deep nesting (nesting depth > option.DefaultMaxInlineDepth),
|
||||
// you can set compile recursive loops in Pretouch for better stability in JIT.
|
||||
option.WithCompileRecursiveDepth(loop),
|
||||
// For a large nested struct, try to set a smaller depth to reduce compiling time.
|
||||
option.WithCompileMaxInlineDepth(depth),
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Copy string
|
||||
|
||||
When decoding **string values without any escaped characters**, sonic references them from the origin JSON buffer instead of mallocing a new buffer to copy. This helps a lot for CPU performance but may leave the whole JSON buffer in memory as long as the decoded objects are being used. In practice, we found the extra memory introduced by referring JSON buffer is usually 20% ~ 80% of decoded objects. Once an application holds these objects for a long time (for example, cache the decoded objects for reusing), its in-use memory on the server may go up. - `Config.CopyString`/`decoder.CopyString()`: We provide the option for `Decode()` / `Unmarshal()` users to choose not to reference the JSON buffer, which may cause a decline in CPU performance to some degree.
|
||||
|
||||
- `GetFromStringNoCopy()`: For memory safety, `sonic.Get()` / `sonic.GetFromString()` now copies return JSON. If users want to get json more quickly and not care about memory usage, you can use `GetFromStringNoCopy()` to return a JSON directly referenced from source.
|
||||
|
||||
### Pass string or []byte?
|
||||
|
||||
For alignment to `encoding/json`, we provide API to pass `[]byte` as an argument, but the string-to-bytes copy is conducted at the same time considering safety, which may lose performance when the origin JSON is huge. Therefore, you can use `UnmarshalString()` and `GetFromString()` to pass a string, as long as your origin data is a string or **nocopy-cast** is safe for your []byte. We also provide API `MarshalString()` for convenient **nocopy-cast** of encoded JSON []byte, which is safe since sonic's output bytes is always duplicated and unique.
|
||||
|
||||
### Accelerate `encoding.TextMarshaler`
|
||||
|
||||
To ensure data security, sonic.Encoder quotes and escapes string values from `encoding.TextMarshaler` interfaces by default, which may degrade performance much if most of your data is in form of them. We provide `encoder.NoQuoteTextMarshaler` to skip these operations, which means you **MUST** ensure their output string escaped and quoted following [RFC8259](https://datatracker.ietf.org/doc/html/rfc8259).
|
||||
|
||||
### Better performance for generic data
|
||||
|
||||
In **fully-parsed** scenario, `Unmarshal()` performs better than `Get()`+`Node.Interface()`. But if you only have a part of the schema for specific json, you can combine `Get()` and `Unmarshal()` together:
|
||||
|
||||
```go
|
||||
import "github.com/bytedance/sonic"
|
||||
|
||||
node, err := sonic.GetFromString(_TwitterJson, "statuses", 3, "user")
|
||||
var user User // your partial schema...
|
||||
err = sonic.UnmarshalString(node.Raw(), &user)
|
||||
```
|
||||
|
||||
Even if you don't have any schema, use `ast.Node` as the container of generic values instead of `map` or `interface`:
|
||||
|
||||
```go
|
||||
import "github.com/bytedance/sonic"
|
||||
|
||||
root, err := sonic.GetFromString(_TwitterJson)
|
||||
user := root.GetByPath("statuses", 3, "user") // === root.Get("status").Index(3).Get("user")
|
||||
err = user.Check()
|
||||
|
||||
// err = user.LoadAll() // only call this when you want to use 'user' concurrently...
|
||||
go someFunc(user)
|
||||
```
|
||||
|
||||
Why? Because `ast.Node` stores its children using `array`:
|
||||
|
||||
- `Array`'s performance is **much better** than `Map` when Inserting (Deserialize) and Scanning (Serialize) data;
|
||||
- **Hashing** (`map[x]`) is not as efficient as **Indexing** (`array[x]`), which `ast.Node` can conduct on **both array and object**;
|
||||
- Using `Interface()`/`Map()` means Sonic must parse all the underlying values, while `ast.Node` can parse them **on demand**.
|
||||
|
||||
**CAUTION:** `ast.Node` **DOESN'T** ensure concurrent security directly, due to its **lazy-load** design. However, you can call `Node.Load()`/`Node.LoadAll()` to achieve that, which may bring performance reduction while it still works faster than converting to `map` or `interface{}`
|
||||
|
||||
### Ast.Node or Ast.Visitor?
|
||||
|
||||
For generic data, `ast.Node` should be enough for your needs in most cases.
|
||||
|
||||
However, `ast.Node` is designed for partially processing JSON string. It has some special designs such as lazy-load which might not be suitable for directly parsing the whole JSON string like `Unmarshal()`. Although `ast.Node` is better then `map` or `interface{}`, it's also a kind of intermediate representation after all if your final types are customized and you have to convert the above types to your custom types after parsing.
|
||||
|
||||
For better performance, in previous case the `ast.Visitor` will be the better choice. It performs JSON decoding like `Unmarshal()` and you can directly use your final types to represents a JSON AST without any intermediate representations.
|
||||
|
||||
But `ast.Visitor` is not a very handy API. You might need to write a lot of code to implement your visitor and carefully maintain the tree hierarchy during decoding. Please read the comments in [ast/visitor.go](https://github.com/bytedance/sonic/blob/main/ast/visitor.go) carefully if you decide to use this API.
|
||||
|
||||
### Buffer Size
|
||||
|
||||
Sonic use memory pool in many places like `encoder.Encode`, `ast.Node.MarshalJSON` to improve performance, which may produce more memory usage (in-use) when server's load is high. See [issue 614](https://github.com/bytedance/sonic/issues/614). Therefore, we introduce some options to let user control the behavior of memory pool. See [option](https://pkg.go.dev/github.com/bytedance/[email protected]/option#pkg-variables) package.
|
||||
|
||||
### Faster JSON Skip
|
||||
|
||||
For security, sonic use [FSM](native/skip_one.c) algorithm to validate JSON when decoding raw JSON or encoding `json.Marshaler`, which is much slower (1~10x) than [SIMD-searching-pair](native/skip_one_fast.c) algorithm. If user has many redundant JSON value and DO NOT NEED to strictly validate JSON correctness, you can enable below options:
|
||||
|
||||
- `Config.NoValidateSkipJSON`: for faster skipping JSON when decoding, such as unknown fields, json.Unmarshaler(json.RawMessage), mismatched values, and redundant array elements
|
||||
- `Config.NoValidateJSONMarshaler`: avoid validating JSON when encoding `json.Marshaler`
|
||||
- `SearchOption.ValidateJSON`: indicates if validate located JSON value when `Get`
|
||||
|
||||
## JSON-Path Support (GJSON)
|
||||
|
||||
[tidwall/gjson](https://github.com/tidwall/gjson) has provided a comprehensive and popular JSON-Path API, and
|
||||
a lot of older codes heavily relies on it. Therefore, we provides a wrapper library, which combines gjson's API with sonic's SIMD algorithm to boost up the performance. See [cloudwego/gjson](https://github.com/cloudwego/gjson).
|
||||
|
||||
## Community
|
||||
|
||||
Sonic is a subproject of [CloudWeGo](https://www.cloudwego.io/). We are committed to building a cloud native ecosystem.
|
||||
@@ -0,0 +1,494 @@
|
||||
# Sonic
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
一个速度奇快的 JSON 序列化/反序列化库,由 JIT (即时编译)和 SIMD (单指令流多数据流)加速。
|
||||
|
||||
## 依赖
|
||||
|
||||
- Go: 1.18~1.25
|
||||
- 注意:Go1.24.0 由于 [issue](https://github.com/golang/go/issues/71672) 不可用,请升级到更高 Go 版本,或添加编译选项 `--ldflags="-checklinkname=0"`
|
||||
- OS: Linux / MacOS / Windows
|
||||
- CPU: AMD64 / (ARM64, 需要 Go1.20 以上)
|
||||
|
||||
## 接口
|
||||
|
||||
详见 [go.dev](https://pkg.go.dev/github.com/bytedance/sonic)
|
||||
|
||||
## 特色
|
||||
|
||||
- 运行时对象绑定,无需代码生成
|
||||
- 完备的 JSON 操作 API
|
||||
- 快,更快,还要更快!
|
||||
|
||||
## 基准测试
|
||||
|
||||
对于**所有大小**的 json 和**所有使用场景**, **Sonic 表现均为最佳**。
|
||||
|
||||
- [中型](https://github.com/bytedance/sonic/blob/main/decoder/testdata_test.go#L19) (13kB, 300+ 键, 6 层)
|
||||
|
||||
```powershell
|
||||
goversion: 1.17.1
|
||||
goos: darwin
|
||||
goarch: amd64
|
||||
cpu: Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz
|
||||
BenchmarkEncoder_Generic_Sonic-16 32393 ns/op 402.40 MB/s 11965 B/op 4 allocs/op
|
||||
BenchmarkEncoder_Generic_Sonic_Fast-16 21668 ns/op 601.57 MB/s 10940 B/op 4 allocs/op
|
||||
BenchmarkEncoder_Generic_JsonIter-16 42168 ns/op 309.12 MB/s 14345 B/op 115 allocs/op
|
||||
BenchmarkEncoder_Generic_GoJson-16 65189 ns/op 199.96 MB/s 23261 B/op 16 allocs/op
|
||||
BenchmarkEncoder_Generic_StdLib-16 106322 ns/op 122.60 MB/s 49136 B/op 789 allocs/op
|
||||
BenchmarkEncoder_Binding_Sonic-16 6269 ns/op 2079.26 MB/s 14173 B/op 4 allocs/op
|
||||
BenchmarkEncoder_Binding_Sonic_Fast-16 5281 ns/op 2468.16 MB/s 12322 B/op 4 allocs/op
|
||||
BenchmarkEncoder_Binding_JsonIter-16 20056 ns/op 649.93 MB/s 9488 B/op 2 allocs/op
|
||||
BenchmarkEncoder_Binding_GoJson-16 8311 ns/op 1568.32 MB/s 9481 B/op 1 allocs/op
|
||||
BenchmarkEncoder_Binding_StdLib-16 16448 ns/op 792.52 MB/s 9479 B/op 1 allocs/op
|
||||
BenchmarkEncoder_Parallel_Generic_Sonic-16 6681 ns/op 1950.93 MB/s 12738 B/op 4 allocs/op
|
||||
BenchmarkEncoder_Parallel_Generic_Sonic_Fast-16 4179 ns/op 3118.99 MB/s 10757 B/op 4 allocs/op
|
||||
BenchmarkEncoder_Parallel_Generic_JsonIter-16 9861 ns/op 1321.84 MB/s 14362 B/op 115 allocs/op
|
||||
BenchmarkEncoder_Parallel_Generic_GoJson-16 18850 ns/op 691.52 MB/s 23278 B/op 16 allocs/op
|
||||
BenchmarkEncoder_Parallel_Generic_StdLib-16 45902 ns/op 283.97 MB/s 49174 B/op 789 allocs/op
|
||||
BenchmarkEncoder_Parallel_Binding_Sonic-16 1480 ns/op 8810.09 MB/s 13049 B/op 4 allocs/op
|
||||
BenchmarkEncoder_Parallel_Binding_Sonic_Fast-16 1209 ns/op 10785.23 MB/s 11546 B/op 4 allocs/op
|
||||
BenchmarkEncoder_Parallel_Binding_JsonIter-16 6170 ns/op 2112.58 MB/s 9504 B/op 2 allocs/op
|
||||
BenchmarkEncoder_Parallel_Binding_GoJson-16 3321 ns/op 3925.52 MB/s 9496 B/op 1 allocs/op
|
||||
BenchmarkEncoder_Parallel_Binding_StdLib-16 3739 ns/op 3486.49 MB/s 9480 B/op 1 allocs/op
|
||||
|
||||
BenchmarkDecoder_Generic_Sonic-16 66812 ns/op 195.10 MB/s 57602 B/op 723 allocs/op
|
||||
BenchmarkDecoder_Generic_Sonic_Fast-16 54523 ns/op 239.07 MB/s 49786 B/op 313 allocs/op
|
||||
BenchmarkDecoder_Generic_StdLib-16 124260 ns/op 104.90 MB/s 50869 B/op 772 allocs/op
|
||||
BenchmarkDecoder_Generic_JsonIter-16 91274 ns/op 142.81 MB/s 55782 B/op 1068 allocs/op
|
||||
BenchmarkDecoder_Generic_GoJson-16 88569 ns/op 147.17 MB/s 66367 B/op 973 allocs/op
|
||||
BenchmarkDecoder_Binding_Sonic-16 32557 ns/op 400.38 MB/s 28302 B/op 137 allocs/op
|
||||
BenchmarkDecoder_Binding_Sonic_Fast-16 28649 ns/op 455.00 MB/s 24999 B/op 34 allocs/op
|
||||
BenchmarkDecoder_Binding_StdLib-16 111437 ns/op 116.97 MB/s 10576 B/op 208 allocs/op
|
||||
BenchmarkDecoder_Binding_JsonIter-16 35090 ns/op 371.48 MB/s 14673 B/op 385 allocs/op
|
||||
BenchmarkDecoder_Binding_GoJson-16 28738 ns/op 453.59 MB/s 22039 B/op 49 allocs/op
|
||||
BenchmarkDecoder_Parallel_Generic_Sonic-16 12321 ns/op 1057.91 MB/s 57233 B/op 723 allocs/op
|
||||
BenchmarkDecoder_Parallel_Generic_Sonic_Fast-16 10644 ns/op 1224.64 MB/s 49362 B/op 313 allocs/op
|
||||
BenchmarkDecoder_Parallel_Generic_StdLib-16 57587 ns/op 226.35 MB/s 50874 B/op 772 allocs/op
|
||||
BenchmarkDecoder_Parallel_Generic_JsonIter-16 38666 ns/op 337.12 MB/s 55789 B/op 1068 allocs/op
|
||||
BenchmarkDecoder_Parallel_Generic_GoJson-16 30259 ns/op 430.79 MB/s 66370 B/op 974 allocs/op
|
||||
BenchmarkDecoder_Parallel_Binding_Sonic-16 5965 ns/op 2185.28 MB/s 27747 B/op 137 allocs/op
|
||||
BenchmarkDecoder_Parallel_Binding_Sonic_Fast-16 5170 ns/op 2521.31 MB/s 24715 B/op 34 allocs/op
|
||||
BenchmarkDecoder_Parallel_Binding_StdLib-16 27582 ns/op 472.58 MB/s 10576 B/op 208 allocs/op
|
||||
BenchmarkDecoder_Parallel_Binding_JsonIter-16 13571 ns/op 960.51 MB/s 14685 B/op 385 allocs/op
|
||||
BenchmarkDecoder_Parallel_Binding_GoJson-16 10031 ns/op 1299.51 MB/s 22111 B/op 49 allocs/op
|
||||
|
||||
BenchmarkGetOne_Sonic-16 3276 ns/op 3975.78 MB/s 24 B/op 1 allocs/op
|
||||
BenchmarkGetOne_Gjson-16 9431 ns/op 1380.81 MB/s 0 B/op 0 allocs/op
|
||||
BenchmarkGetOne_Jsoniter-16 51178 ns/op 254.46 MB/s 27936 B/op 647 allocs/op
|
||||
BenchmarkGetOne_Parallel_Sonic-16 216.7 ns/op 60098.95 MB/s 24 B/op 1 allocs/op
|
||||
BenchmarkGetOne_Parallel_Gjson-16 1076 ns/op 12098.62 MB/s 0 B/op 0 allocs/op
|
||||
BenchmarkGetOne_Parallel_Jsoniter-16 17741 ns/op 734.06 MB/s 27945 B/op 647 allocs/op
|
||||
BenchmarkSetOne_Sonic-16 9571 ns/op 1360.61 MB/s 1584 B/op 17 allocs/op
|
||||
BenchmarkSetOne_Sjson-16 36456 ns/op 357.22 MB/s 52180 B/op 9 allocs/op
|
||||
BenchmarkSetOne_Jsoniter-16 79475 ns/op 163.86 MB/s 45862 B/op 964 allocs/op
|
||||
BenchmarkSetOne_Parallel_Sonic-16 850.9 ns/op 15305.31 MB/s 1584 B/op 17 allocs/op
|
||||
BenchmarkSetOne_Parallel_Sjson-16 18194 ns/op 715.77 MB/s 52247 B/op 9 allocs/op
|
||||
BenchmarkSetOne_Parallel_Jsoniter-16 33560 ns/op 388.05 MB/s 45892 B/op 964 allocs/op
|
||||
BenchmarkLoadNode/LoadAll()-16 11384 ns/op 1143.93 MB/s 6307 B/op 25 allocs/op
|
||||
BenchmarkLoadNode_Parallel/LoadAll()-16 5493 ns/op 2370.68 MB/s 7145 B/op 25 allocs/op
|
||||
BenchmarkLoadNode/Interface()-16 17722 ns/op 734.85 MB/s 13323 B/op 88 allocs/op
|
||||
BenchmarkLoadNode_Parallel/Interface()-16 10330 ns/op 1260.70 MB/s 15178 B/op 88 allocs/op
|
||||
```
|
||||
|
||||
- [小型](https://github.com/bytedance/sonic/blob/main/testdata/small.go) (400B, 11 个键, 3 层)
|
||||

|
||||
- [大型](https://github.com/bytedance/sonic/blob/main/testdata/twitter.json) (635kB, 10000+ 个键, 6 层)
|
||||

|
||||
|
||||
要查看基准测试代码,请参阅 [bench.sh](https://github.com/bytedance/sonic/blob/main/scripts/bench.sh) 。
|
||||
|
||||
## 工作原理
|
||||
|
||||
请参阅 [INTRODUCTION_ZH_CN.md](./docs/INTRODUCTION_ZH_CN.md).
|
||||
|
||||
## 使用方式
|
||||
|
||||
### 序列化/反序列化
|
||||
|
||||
默认的行为基本上与 `encoding/json` 相一致,除了 HTML 转义形式(参见 [Escape HTML](https://github.com/bytedance/sonic/blob/main/README.md#escape-html)) 和 `SortKeys` 功能(参见 [Sort Keys](https://github.com/bytedance/sonic/blob/main/README.md#sort-keys))**没有**遵循 [RFC8259](https://datatracker.ietf.org/doc/html/rfc8259) 。
|
||||
|
||||
```go
|
||||
import "github.com/bytedance/sonic"
|
||||
|
||||
var data YourSchema
|
||||
// Marshal
|
||||
output, err := sonic.Marshal(&data)
|
||||
// Unmarshal
|
||||
err := sonic.Unmarshal(output, &data)
|
||||
```
|
||||
|
||||
### 流式输入输出
|
||||
|
||||
Sonic 支持解码 `io.Reader` 中输入的 json,或将对象编码为 json 后输出至 `io.Writer`,以处理多个值并减少内存消耗。
|
||||
|
||||
- 编码器
|
||||
|
||||
```go
|
||||
var o1 = map[string]interface{}{
|
||||
"a": "b",
|
||||
}
|
||||
var o2 = 1
|
||||
var w = bytes.NewBuffer(nil)
|
||||
var enc = sonic.ConfigDefault.NewEncoder(w)
|
||||
enc.Encode(o1)
|
||||
enc.Encode(o2)
|
||||
fmt.Println(w.String())
|
||||
// Output:
|
||||
// {"a":"b"}
|
||||
// 1
|
||||
```
|
||||
|
||||
- 解码器
|
||||
|
||||
```go
|
||||
var o = map[string]interface{}{}
|
||||
var r = strings.NewReader(`{"a":"b"}{"1":"2"}`)
|
||||
var dec = sonic.ConfigDefault.NewDecoder(r)
|
||||
dec.Decode(&o)
|
||||
dec.Decode(&o)
|
||||
fmt.Printf("%+v", o)
|
||||
// Output:
|
||||
// map[1:2 a:b]
|
||||
```
|
||||
|
||||
### 使用 `Number` / `int64`
|
||||
|
||||
```go
|
||||
import "github.com/bytedance/sonic/decoder"
|
||||
|
||||
var input = `1`
|
||||
var data interface{}
|
||||
|
||||
// default float64
|
||||
dc := decoder.NewDecoder(input)
|
||||
dc.Decode(&data) // data == float64(1)
|
||||
// use json.Number
|
||||
dc = decoder.NewDecoder(input)
|
||||
dc.UseNumber()
|
||||
dc.Decode(&data) // data == json.Number("1")
|
||||
// use int64
|
||||
dc = decoder.NewDecoder(input)
|
||||
dc.UseInt64()
|
||||
dc.Decode(&data) // data == int64(1)
|
||||
|
||||
root, err := sonic.GetFromString(input)
|
||||
// Get json.Number
|
||||
jn := root.Number()
|
||||
jm := root.InterfaceUseNumber().(json.Number) // jn == jm
|
||||
// Get float64
|
||||
fn := root.Float64()
|
||||
fm := root.Interface().(float64) // jn == jm
|
||||
```
|
||||
|
||||
### 对键排序
|
||||
|
||||
考虑到排序带来的性能损失(约 10% ), sonic 默认不会启用这个功能。如果你的组件依赖这个行为(如 [zstd](https://github.com/facebook/zstd)) ,可以仿照下面的例子:
|
||||
|
||||
```go
|
||||
import "github.com/bytedance/sonic"
|
||||
import "github.com/bytedance/sonic/encoder"
|
||||
|
||||
// Binding map only
|
||||
m := map[string]interface{}{}
|
||||
v, err := encoder.Encode(m, encoder.SortMapKeys)
|
||||
|
||||
// Or ast.Node.SortKeys() before marshal
|
||||
var root := sonic.Get(JSON)
|
||||
err := root.SortKeys()
|
||||
```
|
||||
|
||||
### HTML 转义
|
||||
|
||||
考虑到性能损失(约15%), sonic 默认不会启用这个功能。你可以使用 `encoder.EscapeHTML` 选项来开启(与 `encoding/json.HTMLEscape` 行为一致)。
|
||||
|
||||
```go
|
||||
import "github.com/bytedance/sonic"
|
||||
|
||||
v := map[string]string{"&&":"<>"}
|
||||
ret, err := Encode(v, EscapeHTML) // ret == `{"\u0026\u0026":{"X":"\u003c\u003e"}}`
|
||||
```
|
||||
|
||||
### 紧凑格式
|
||||
|
||||
Sonic 默认将基本类型( `struct` , `map` 等)编码为紧凑格式的 JSON ,除非使用 `json.RawMessage` or `json.Marshaler` 进行编码: sonic 确保输出的 JSON 合法,但出于性能考虑,**不会**加工成紧凑格式。我们提供选项 `encoder.CompactMarshaler` 来添加此过程,
|
||||
|
||||
### 打印错误
|
||||
|
||||
如果输入的 JSON 存在无效的语法,sonic 将返回 `decoder.SyntaxError`,该错误支持错误位置的美化输出。
|
||||
|
||||
```go
|
||||
import "github.com/bytedance/sonic"
|
||||
import "github.com/bytedance/sonic/decoder"
|
||||
|
||||
var data interface{}
|
||||
err := sonic.UnmarshalString("[[[}]]", &data)
|
||||
if err != nil {
|
||||
/* One line by default */
|
||||
println(e.Error()) // "Syntax error at index 3: invalid char\n\n\t[[[}]]\n\t...^..\n"
|
||||
/* Pretty print */
|
||||
if e, ok := err.(decoder.SyntaxError); ok {
|
||||
/*Syntax error at index 3: invalid char
|
||||
|
||||
[[[}]]
|
||||
...^..
|
||||
*/
|
||||
print(e.Description())
|
||||
} else if me, ok := err.(*decoder.MismatchTypeError); ok {
|
||||
// decoder.MismatchTypeError is new to Sonic v1.6.0
|
||||
print(me.Description())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 类型不匹配 [Sonic v1.6.0]
|
||||
|
||||
如果给定键中存在**类型不匹配**的值, sonic 会抛出 `decoder.MismatchTypeError` (如果有多个,只会报告最后一个),但仍会跳过错误的值并解码下一个 JSON 。
|
||||
|
||||
```go
|
||||
import "github.com/bytedance/sonic"
|
||||
import "github.com/bytedance/sonic/decoder"
|
||||
|
||||
var data = struct{
|
||||
A int
|
||||
B int
|
||||
}{}
|
||||
err := UnmarshalString(`{"A":"1","B":1}`, &data)
|
||||
println(err.Error()) // Mismatch type int with value string "at index 5: mismatched type with value\n\n\t{\"A\":\"1\",\"B\":1}\n\t.....^.........\n"
|
||||
fmt.Printf("%+v", data) // {A:0 B:1}
|
||||
```
|
||||
|
||||
### `Ast.Node`
|
||||
|
||||
Sonic/ast.Node 是完全独立的 JSON 抽象语法树库。它实现了序列化和反序列化,并提供了获取和修改JSON数据的鲁棒的 API。
|
||||
|
||||
#### 查找/索引
|
||||
|
||||
通过给定的路径搜索 JSON 片段,路径必须为非负整数,字符串或 `nil` 。
|
||||
|
||||
```go
|
||||
import "github.com/bytedance/sonic"
|
||||
|
||||
input := []byte(`{"key1":[{},{"key2":{"key3":[1,2,3]}}]}`)
|
||||
|
||||
// no path, returns entire json
|
||||
root, err := sonic.Get(input)
|
||||
raw := root.Raw() // == string(input)
|
||||
|
||||
// multiple paths
|
||||
root, err := sonic.Get(input, "key1", 1, "key2")
|
||||
sub := root.Get("key3").Index(2).Int64() // == 3
|
||||
```
|
||||
|
||||
**注意**:由于 `Index()` 使用偏移量来定位数据,比使用扫描的 `Get()` 要快的多,建议尽可能的使用 `Index` 。 Sonic 也提供了另一个 API, `IndexOrGet()` ,以偏移量为基础并且也确保键的匹配。
|
||||
|
||||
#### 查找选项
|
||||
|
||||
`ast.Searcher`提供了一些选项,以满足用户的不同需求:
|
||||
|
||||
```go
|
||||
opts := ast.SearchOption{CopyReturn: true…}
|
||||
val, err := sonic.GetWithOptions(JSON, opts, "key")
|
||||
```
|
||||
|
||||
- CopyReturn
|
||||
指示搜索器复制结果JSON字符串,而不是从输入引用。如果用户缓存结果,这有助于减少内存使用
|
||||
- ConcurentRead
|
||||
因为`ast.Node`使用`Lazy-Load`设计,默认不支持并发读取。如果您想同时读取,请指定它。
|
||||
- ValidateJSON
|
||||
指示搜索器来验证整个JSON。默认情况下启用该选项, 但是对于查找速度有一定影响。
|
||||
|
||||
#### 修改
|
||||
|
||||
使用 `Set()` / `Unset()` 修改 json 的内容
|
||||
|
||||
```go
|
||||
import "github.com/bytedance/sonic"
|
||||
|
||||
// Set
|
||||
exist, err := root.Set("key4", NewBool(true)) // exist == false
|
||||
alias1 := root.Get("key4")
|
||||
println(alias1.Valid()) // true
|
||||
alias2 := root.Index(1)
|
||||
println(alias1 == alias2) // true
|
||||
|
||||
// Unset
|
||||
exist, err := root.UnsetByIndex(1) // exist == true
|
||||
println(root.Get("key4").Check()) // "value not exist"
|
||||
```
|
||||
|
||||
#### 序列化
|
||||
|
||||
要将 `ast.Node` 编码为 json ,使用 `MarshalJson()` 或者 `json.Marshal()` (必须传递指向节点的指针)
|
||||
|
||||
```go
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/bytedance/sonic"
|
||||
)
|
||||
|
||||
buf, err := root.MarshalJson()
|
||||
println(string(buf)) // {"key1":[{},{"key2":{"key3":[1,2,3]}}]}
|
||||
exp, err := json.Marshal(&root) // WARN: use pointer
|
||||
println(string(buf) == string(exp)) // true
|
||||
```
|
||||
|
||||
#### APIs
|
||||
|
||||
- 合法性检查: `Check()`, `Error()`, `Valid()`, `Exist()`
|
||||
- 索引: `Index()`, `Get()`, `IndexPair()`, `IndexOrGet()`, `GetByPath()`
|
||||
- 转换至 go 内置类型: `Int64()`, `Float64()`, `String()`, `Number()`, `Bool()`, `Map[UseNumber|UseNode]()`, `Array[UseNumber|UseNode]()`, `Interface[UseNumber|UseNode]()`
|
||||
- go 类型打包: `NewRaw()`, `NewNumber()`, `NewNull()`, `NewBool()`, `NewString()`, `NewObject()`, `NewArray()`
|
||||
- 迭代: `Values()`, `Properties()`, `ForEach()`, `SortKeys()`
|
||||
- 修改: `Set()`, `SetByIndex()`, `Add()`
|
||||
|
||||
### `Ast.Visitor`
|
||||
|
||||
Sonic 提供了一个高级的 API 用于直接全量解析 JSON 到非标准容器里 (既不是 `struct` 也不是 `map[string]interface{}`) 且不需要借助任何中间表示 (`ast.Node` 或 `interface{}`)。举个例子,你可能定义了下述的类型,它们看起来像 `interface{}`,但实际上并不是:
|
||||
|
||||
```go
|
||||
type UserNode interface {}
|
||||
|
||||
// the following types implement the UserNode interface.
|
||||
type (
|
||||
UserNull struct{}
|
||||
UserBool struct{ Value bool }
|
||||
UserInt64 struct{ Value int64 }
|
||||
UserFloat64 struct{ Value float64 }
|
||||
UserString struct{ Value string }
|
||||
UserObject struct{ Value map[string]UserNode }
|
||||
UserArray struct{ Value []UserNode }
|
||||
)
|
||||
```
|
||||
|
||||
Sonic 提供了下述的 API 来返回 **“对 JSON AST 的前序遍历”**。`ast.Visitor` 是一个 SAX 风格的接口,这在某些 C++ 的 JSON 解析库中被使用到。你需要自己实现一个 `ast.Visitor`,将它传递给 `ast.Preorder()` 方法。在你的实现中你可以使用自定义的类型来表示 JSON 的值。在你的 `ast.Visitor` 中,可能需要有一个 O(n) 空间复杂度的容器(比如说栈)来记录 object / array 的层级。
|
||||
|
||||
```go
|
||||
func Preorder(str string, visitor Visitor, opts *VisitorOptions) error
|
||||
|
||||
type Visitor interface {
|
||||
OnNull() error
|
||||
OnBool(v bool) error
|
||||
OnString(v string) error
|
||||
OnInt64(v int64, n json.Number) error
|
||||
OnFloat64(v float64, n json.Number) error
|
||||
OnObjectBegin(capacity int) error
|
||||
OnObjectKey(key string) error
|
||||
OnObjectEnd() error
|
||||
OnArrayBegin(capacity int) error
|
||||
OnArrayEnd() error
|
||||
}
|
||||
```
|
||||
|
||||
详细用法参看 [ast/visitor.go](https://github.com/bytedance/sonic/blob/main/ast/visitor.go),我们还为 `UserNode` 实现了一个示例 `ast.Visitor`,你可以在 [ast/visitor_test.go](https://github.com/bytedance/sonic/blob/main/ast/visitor_test.go) 中找到它。
|
||||
|
||||
## 兼容性
|
||||
|
||||
对于想要使用sonic来满足不同场景的开发人员,我们提供了一些集成配置:
|
||||
|
||||
- `ConfigDefault`: sonic的默认配置 (`EscapeHTML=false`, `SortKeys=false`…) 保证性能同时兼顾安全性。
|
||||
- `ConfigStd`: 与 `encoding/json` 保证完全兼容的配置
|
||||
- `ConfigFastest`: 最快的配置(`NoQuoteTextMarshaler=true...`) 保证性能最优但是会缺少一些安全性检查(validate UTF8 等)
|
||||
Sonic **不**确保支持所有环境,由于开发高性能代码的困难。在不支持sonic的环境中,实现将回落到 `encoding/json`。因此上述配置将全部等于`ConfigStd`。
|
||||
|
||||
## 注意事项
|
||||
|
||||
### 预热
|
||||
|
||||
由于 Sonic 使用 [golang-asm](https://github.com/twitchyliquid64/golang-asm) 作为 JIT 汇编器,这个库并不适用于运行时编译,第一次运行一个大型模式可能会导致请求超时甚至进程内存溢出。为了更好地稳定性,我们建议在运行大型模式或在内存有限的应用中,在使用 `Marshal()/Unmarshal()` 前运行 `Pretouch()`。
|
||||
|
||||
```go
|
||||
import (
|
||||
"reflect"
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/bytedance/sonic/option"
|
||||
)
|
||||
|
||||
func init() {
|
||||
var v HugeStruct
|
||||
|
||||
// For most large types (nesting depth <= option.DefaultMaxInlineDepth)
|
||||
err := sonic.Pretouch(reflect.TypeOf(v))
|
||||
|
||||
// with more CompileOption...
|
||||
err := sonic.Pretouch(reflect.TypeOf(v),
|
||||
// If the type is too deep nesting (nesting depth > option.DefaultMaxInlineDepth),
|
||||
// you can set compile recursive loops in Pretouch for better stability in JIT.
|
||||
option.WithCompileRecursiveDepth(loop),
|
||||
// For a large nested struct, try to set a smaller depth to reduce compiling time.
|
||||
option.WithCompileMaxInlineDepth(depth),
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 拷贝字符串
|
||||
|
||||
当解码 **没有转义字符的字符串**时, sonic 会从原始的 JSON 缓冲区内引用而不是复制到新的一个缓冲区中。这对 CPU 的性能方面很有帮助,但是可能因此在解码后对象仍在使用的时候将整个 JSON 缓冲区保留在内存中。实践中我们发现,通过引用 JSON 缓冲区引入的额外内存通常是解码后对象的 20% 至 80% ,一旦应用长期保留这些对象(如缓存以备重用),服务器所使用的内存可能会增加。我们提供了选项 `decoder.CopyString()` 供用户选择,不引用 JSON 缓冲区。这可能在一定程度上降低 CPU 性能。
|
||||
|
||||
### 传递字符串还是字节数组?
|
||||
|
||||
为了和 `encoding/json` 保持一致,我们提供了传递 `[]byte` 作为参数的 API ,但考虑到安全性,字符串到字节的复制是同时进行的,这在原始 JSON 非常大时可能会导致性能损失。因此,你可以使用 `UnmarshalString()` 和 `GetFromString()` 来传递字符串,只要你的原始数据是字符串,或**零拷贝类型转换**对于你的字节数组是安全的。我们也提供了 `MarshalString()` 的 API ,以便对编码的 JSON 字节数组进行**零拷贝类型转换**,因为 sonic 输出的字节始终是重复并且唯一的,所以这样是安全的。
|
||||
|
||||
### 加速 `encoding.TextMarshaler`
|
||||
|
||||
为了保证数据安全性, `sonic.Encoder` 默认会对来自 `encoding.TextMarshaler` 接口的字符串进行引用和转义,如果大部分数据都是这种形式那可能会导致很大的性能损失。我们提供了 `encoder.NoQuoteTextMarshaler` 选项来跳过这些操作,但你**必须**保证他们的输出字符串依照 [RFC8259](https://datatracker.ietf.org/doc/html/rfc8259) 进行了转义和引用。
|
||||
|
||||
### 泛型的性能优化
|
||||
|
||||
在 **完全解析**的场景下, `Unmarshal()` 表现得比 `Get()`+`Node.Interface()` 更好。但是如果你只有特定 JSON 的部分模式,你可以将 `Get()` 和 `Unmarshal()` 结合使用:
|
||||
|
||||
```go
|
||||
import "github.com/bytedance/sonic"
|
||||
|
||||
node, err := sonic.GetFromString(_TwitterJson, "statuses", 3, "user")
|
||||
var user User // your partial schema...
|
||||
err = sonic.UnmarshalString(node.Raw(), &user)
|
||||
```
|
||||
|
||||
甚至如果你没有任何模式,可以用 `ast.Node` 代替 `map` 或 `interface` 作为泛型的容器:
|
||||
|
||||
```go
|
||||
import "github.com/bytedance/sonic"
|
||||
|
||||
root, err := sonic.GetFromString(_TwitterJson)
|
||||
user := root.GetByPath("statuses", 3, "user") // === root.Get("status").Index(3).Get("user")
|
||||
err = user.Check()
|
||||
|
||||
// err = user.LoadAll() // only call this when you want to use 'user' concurrently...
|
||||
go someFunc(user)
|
||||
```
|
||||
|
||||
为什么?因为 `ast.Node` 使用 `array` 来存储其子节点:
|
||||
|
||||
- 在插入(反序列化)和扫描(序列化)数据时,`Array` 的性能比 `Map` **好得多**;
|
||||
- **哈希**(`map[x]`)的效率不如**索引**(`array[x]`)高效,而 `ast.Node` 可以在数组和对象上使用索引;
|
||||
- 使用 `Interface()` / `Map()` 意味着 sonic 必须解析所有的底层值,而 `ast.Node` 可以**按需解析**它们。
|
||||
|
||||
**注意**:由于 `ast.Node` 的惰性加载设计,其**不能**直接保证并发安全性,但你可以调用 `Node.Load()` / `Node.LoadAll()` 来实现并发安全。尽管可能会带来性能损失,但仍比转换成 `map` 或 `interface{}` 更为高效。
|
||||
|
||||
### 使用 `ast.Node` 还是 `ast.Visitor`?
|
||||
|
||||
对于泛型数据的解析,`ast.Node` 在大多数场景上应该能够满足你的需求。
|
||||
|
||||
然而,`ast.Node` 是一种针对部分解析 JSON 而设计的泛型容器,它包含一些特殊设计,比如惰性加载,如果你希望像 `Unmarshal()` 那样直接解析整个 JSON,这些设计可能并不合适。尽管 `ast.Node` 相较于 `map` 或 `interface{}` 来说是更好的一种泛型容器,但它毕竟也是一种中间表示,如果你的最终类型是自定义的,你还得在解析完成后将上述类型转化成你自定义的类型。
|
||||
|
||||
在上述场景中,如果想要有更极致的性能,`ast.Visitor` 会是更好的选择。它采用和 `Unmarshal()` 类似的形式解析 JSON,并且你可以直接使用你的最终类型去表示 JSON AST,而不需要经过额外的任何中间表示。
|
||||
|
||||
但是,`ast.Visitor` 并不是一个很易用的 API。你可能需要写大量的代码去实现自己的 `ast.Visitor`,并且需要在解析过程中仔细维护树的层级。如果你决定要使用这个 API,请先仔细阅读 [ast/visitor.go](https://github.com/bytedance/sonic/blob/main/ast/visitor.go) 中的注释。
|
||||
|
||||
### 缓冲区大小
|
||||
|
||||
Sonic在许多地方使用内存池,如`encoder.Encode`, `ast.Node.MarshalJSON`等来提高性能,这可能会在服务器负载高时产生更多的内存使用(in-use)。参见[issue 614](https://github.com/bytedance/sonic/issues/614)。因此,我们引入了一些选项来让用户配置内存池的行为。参见[option](https://pkg.go.dev/github.com/bytedance/[email protected]/option#pkg-variables)包。
|
||||
|
||||
### 更快的 JSON Skip
|
||||
|
||||
为了安全起见,在跳过原始JSON 时,sonic decoder 默认使用[FSM](native/skip_one.c)算法扫描来跳过同时校验 JSON。它相比[SIMD-searching-pair](native/skip_one_fast.c)算法跳过要慢得多(1~10倍)。如果用户有很多冗余的JSON值,并且不需要严格验证JSON的正确性,你可以启用以下选项:
|
||||
|
||||
- `Config.NoValidateSkipJSON`: 用于在解码时更快地跳过JSON,例如未知字段,`json.RawMessage`,不匹配的值和冗余的数组元素等
|
||||
- `Config.NoValidateJSONMarshaler`: 编码JSON时避免验证JSON。封送拆收器
|
||||
- `SearchOption.ValidateJSON`: 指示当`Get`时是否验证定位的JSON值
|
||||
|
||||
## 社区
|
||||
|
||||
Sonic 是 [CloudWeGo](https://www.cloudwego.io/) 下的一个子项目。我们致力于构建云原生生态系统。
|
||||
@@ -0,0 +1,249 @@
|
||||
/*
|
||||
* Copyright 2021 ByteDance Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package sonic
|
||||
|
||||
import (
|
||||
`io`
|
||||
|
||||
`github.com/bytedance/sonic/ast`
|
||||
`github.com/bytedance/sonic/internal/rt`
|
||||
)
|
||||
|
||||
const (
|
||||
// UseStdJSON indicates you are using fallback implementation (encoding/json)
|
||||
UseStdJSON = iota
|
||||
// UseSonicJSON indicates you are using real sonic implementation
|
||||
UseSonicJSON
|
||||
)
|
||||
|
||||
// APIKind is the kind of API, 0 is std json, 1 is sonic.
|
||||
const APIKind = apiKind
|
||||
|
||||
// Config is a combination of sonic/encoder.Options and sonic/decoder.Options
|
||||
type Config struct {
|
||||
// EscapeHTML indicates encoder to escape all HTML characters
|
||||
// after serializing into JSON (see https://pkg.go.dev/encoding/json#HTMLEscape).
|
||||
// WARNING: This hurts performance A LOT, USE WITH CARE.
|
||||
EscapeHTML bool
|
||||
|
||||
// SortMapKeys indicates encoder that the keys of a map needs to be sorted
|
||||
// before serializing into JSON.
|
||||
// WARNING: This hurts performance A LOT, USE WITH CARE.
|
||||
SortMapKeys bool
|
||||
|
||||
// CompactMarshaler indicates encoder that the output JSON from json.Marshaler
|
||||
// is always compact and needs no validation
|
||||
CompactMarshaler bool
|
||||
|
||||
// NoQuoteTextMarshaler indicates encoder that the output text from encoding.TextMarshaler
|
||||
// is always escaped string and needs no quoting
|
||||
NoQuoteTextMarshaler bool
|
||||
|
||||
// NoNullSliceOrMap indicates encoder that all empty Array or Object are encoded as '[]' or '{}',
|
||||
// instead of 'null'
|
||||
NoNullSliceOrMap bool
|
||||
|
||||
// UseInt64 indicates decoder to unmarshal an integer into an interface{} as an
|
||||
// int64 instead of as a float64.
|
||||
UseInt64 bool
|
||||
|
||||
// UseNumber indicates decoder to unmarshal a number into an interface{} as a
|
||||
// json.Number instead of as a float64.
|
||||
UseNumber bool
|
||||
|
||||
// UseUnicodeErrors indicates decoder to return an error when encounter invalid
|
||||
// UTF-8 escape sequences.
|
||||
UseUnicodeErrors bool
|
||||
|
||||
// DisallowUnknownFields indicates decoder to return an error when the destination
|
||||
// is a struct and the input contains object keys which do not match any
|
||||
// non-ignored, exported fields in the destination.
|
||||
DisallowUnknownFields bool
|
||||
|
||||
// CopyString indicates decoder to decode string values by copying instead of referring.
|
||||
CopyString bool
|
||||
|
||||
// ValidateString indicates decoder and encoder to validate string values: decoder will return errors
|
||||
// when unescaped control chars(\u0000-\u001f) in the string value of JSON.
|
||||
ValidateString bool
|
||||
|
||||
// NoValidateJSONMarshaler indicates that the encoder should not validate the output string
|
||||
// after encoding the JSONMarshaler to JSON.
|
||||
NoValidateJSONMarshaler bool
|
||||
|
||||
// NoValidateJSONSkip indicates the decoder should not validate the JSON value when skipping it,
|
||||
// such as unknown-fields, mismatched-type, redundant elements..
|
||||
NoValidateJSONSkip bool
|
||||
|
||||
// NoEncoderNewline indicates that the encoder should not add a newline after every message
|
||||
NoEncoderNewline bool
|
||||
|
||||
// Encode Infinity or Nan float into `null`, instead of returning an error.
|
||||
EncodeNullForInfOrNan bool
|
||||
|
||||
// CaseSensitive indicates that the decoder should not ignore the case of object keys.
|
||||
CaseSensitive bool
|
||||
}
|
||||
|
||||
var (
|
||||
// ConfigDefault is the default config of APIs, aiming at efficiency and safety.
|
||||
ConfigDefault = Config{}.Froze()
|
||||
|
||||
// ConfigStd is the standard config of APIs, aiming at being compatible with encoding/json.
|
||||
ConfigStd = Config{
|
||||
EscapeHTML : true,
|
||||
SortMapKeys: true,
|
||||
CompactMarshaler: true,
|
||||
CopyString : true,
|
||||
ValidateString : true,
|
||||
}.Froze()
|
||||
|
||||
// ConfigFastest is the fastest config of APIs, aiming at speed.
|
||||
ConfigFastest = Config{
|
||||
NoValidateJSONMarshaler: true,
|
||||
NoValidateJSONSkip: true,
|
||||
}.Froze()
|
||||
)
|
||||
|
||||
|
||||
// API is a binding of specific config.
|
||||
// This interface is inspired by github.com/json-iterator/go,
|
||||
// and has same behaviors under equivalent config.
|
||||
type API interface {
|
||||
// MarshalToString returns the JSON encoding string of v
|
||||
MarshalToString(v interface{}) (string, error)
|
||||
// Marshal returns the JSON encoding bytes of v.
|
||||
Marshal(v interface{}) ([]byte, error)
|
||||
// MarshalIndent returns the JSON encoding bytes with indent and prefix.
|
||||
MarshalIndent(v interface{}, prefix, indent string) ([]byte, error)
|
||||
// UnmarshalFromString parses the JSON-encoded bytes and stores the result in the value pointed to by v.
|
||||
UnmarshalFromString(str string, v interface{}) error
|
||||
// Unmarshal parses the JSON-encoded string and stores the result in the value pointed to by v.
|
||||
Unmarshal(data []byte, v interface{}) error
|
||||
// NewEncoder create a Encoder holding writer
|
||||
NewEncoder(writer io.Writer) Encoder
|
||||
// NewDecoder create a Decoder holding reader
|
||||
NewDecoder(reader io.Reader) Decoder
|
||||
// Valid validates the JSON-encoded bytes and reports if it is valid
|
||||
Valid(data []byte) bool
|
||||
}
|
||||
|
||||
// Encoder encodes JSON into io.Writer
|
||||
type Encoder interface {
|
||||
// Encode writes the JSON encoding of v to the stream, followed by a newline character.
|
||||
Encode(val interface{}) error
|
||||
// SetEscapeHTML specifies whether problematic HTML characters
|
||||
// should be escaped inside JSON quoted strings.
|
||||
// The default behavior NOT ESCAPE
|
||||
SetEscapeHTML(on bool)
|
||||
// SetIndent instructs the encoder to format each subsequent encoded value
|
||||
// as if indented by the package-level function Indent(dst, src, prefix, indent).
|
||||
// Calling SetIndent("", "") disables indentation
|
||||
SetIndent(prefix, indent string)
|
||||
}
|
||||
|
||||
// Decoder decodes JSON from io.Read
|
||||
type Decoder interface {
|
||||
// Decode reads the next JSON-encoded value from its input and stores it in the value pointed to by v.
|
||||
Decode(val interface{}) error
|
||||
// Buffered returns a reader of the data remaining in the Decoder's buffer.
|
||||
// The reader is valid until the next call to Decode.
|
||||
Buffered() io.Reader
|
||||
// DisallowUnknownFields causes the Decoder to return an error when the destination is a struct
|
||||
// and the input contains object keys which do not match any non-ignored, exported fields in the destination.
|
||||
DisallowUnknownFields()
|
||||
// More reports whether there is another element in the current array or object being parsed.
|
||||
More() bool
|
||||
// UseNumber causes the Decoder to unmarshal a number into an interface{} as a Number instead of as a float64.
|
||||
UseNumber()
|
||||
}
|
||||
|
||||
// Marshal returns the JSON encoding bytes of v.
|
||||
func Marshal(val interface{}) ([]byte, error) {
|
||||
return ConfigDefault.Marshal(val)
|
||||
}
|
||||
|
||||
// MarshalIndent is like Marshal but applies Indent to format the output.
|
||||
// Each JSON element in the output will begin on a new line beginning with prefix
|
||||
// followed by one or more copies of indent according to the indentation nesting.
|
||||
func MarshalIndent(v interface{}, prefix, indent string) ([]byte, error) {
|
||||
return ConfigDefault.MarshalIndent(v, prefix, indent)
|
||||
}
|
||||
|
||||
// MarshalString returns the JSON encoding string of v.
|
||||
func MarshalString(val interface{}) (string, error) {
|
||||
return ConfigDefault.MarshalToString(val)
|
||||
}
|
||||
|
||||
// Unmarshal parses the JSON-encoded data and stores the result in the value pointed to by v.
|
||||
// NOTICE: This API copies given buffer by default,
|
||||
// if you want to pass JSON more efficiently, use UnmarshalString instead.
|
||||
func Unmarshal(buf []byte, val interface{}) error {
|
||||
return ConfigDefault.Unmarshal(buf, val)
|
||||
}
|
||||
|
||||
// UnmarshalString is like Unmarshal, except buf is a string.
|
||||
func UnmarshalString(buf string, val interface{}) error {
|
||||
return ConfigDefault.UnmarshalFromString(buf, val)
|
||||
}
|
||||
|
||||
// Get searches and locates the given path from src json,
|
||||
// and returns a ast.Node representing the partially json.
|
||||
//
|
||||
// Each path arg must be integer or string:
|
||||
// - Integer is target index(>=0), means searching current node as array.
|
||||
// - String is target key, means searching current node as object.
|
||||
//
|
||||
//
|
||||
// Notice: It expects the src json is **Well-formed** and **Immutable** when calling,
|
||||
// otherwise it may return unexpected result.
|
||||
// Considering memory safety, the returned JSON is **Copied** from the input
|
||||
func Get(src []byte, path ...interface{}) (ast.Node, error) {
|
||||
return GetCopyFromString(rt.Mem2Str(src), path...)
|
||||
}
|
||||
|
||||
//GetWithOptions searches and locates the given path from src json,
|
||||
// with specific options of ast.Searcher
|
||||
func GetWithOptions(src []byte, opts ast.SearchOptions, path ...interface{}) (ast.Node, error) {
|
||||
s := ast.NewSearcher(rt.Mem2Str(src))
|
||||
s.SearchOptions = opts
|
||||
return s.GetByPath(path...)
|
||||
}
|
||||
|
||||
// GetFromString is same with Get except src is string.
|
||||
//
|
||||
// WARNING: The returned JSON is **Referenced** from the input.
|
||||
// Caching or long-time holding the returned node may cause OOM.
|
||||
// If your src is big, consider use GetFromStringCopy().
|
||||
func GetFromString(src string, path ...interface{}) (ast.Node, error) {
|
||||
return ast.NewSearcher(src).GetByPath(path...)
|
||||
}
|
||||
|
||||
// GetCopyFromString is same with Get except src is string
|
||||
func GetCopyFromString(src string, path ...interface{}) (ast.Node, error) {
|
||||
return ast.NewSearcher(src).GetByPathCopy(path...)
|
||||
}
|
||||
|
||||
// Valid reports whether data is a valid JSON encoding.
|
||||
func Valid(data []byte) bool {
|
||||
return ConfigDefault.Valid(data)
|
||||
}
|
||||
|
||||
// Valid reports whether data is a valid JSON encoding.
|
||||
func ValidString(data string) bool {
|
||||
return ConfigDefault.Valid(rt.Str2Mem(data))
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
//go:build (amd64 && go1.17 && !go1.26) || (arm64 && go1.20 && !go1.26)
|
||||
// +build amd64,go1.17,!go1.26 arm64,go1.20,!go1.26
|
||||
|
||||
/*
|
||||
* Copyright 2022 ByteDance Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ast
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"unsafe"
|
||||
|
||||
"github.com/bytedance/sonic/encoder"
|
||||
"github.com/bytedance/sonic/internal/encoder/alg"
|
||||
"github.com/bytedance/sonic/internal/native"
|
||||
"github.com/bytedance/sonic/internal/native/types"
|
||||
"github.com/bytedance/sonic/internal/rt"
|
||||
"github.com/bytedance/sonic/utf8"
|
||||
)
|
||||
|
||||
var typeByte = rt.UnpackEface(byte(0)).Type
|
||||
|
||||
func quote(buf *[]byte, val string) {
|
||||
*buf = alg.Quote(*buf, val, false)
|
||||
}
|
||||
|
||||
func (self *Parser) decodeValue() (val types.JsonState) {
|
||||
sv := (*rt.GoString)(unsafe.Pointer(&self.s))
|
||||
flag := types.F_USE_NUMBER
|
||||
if self.dbuf != nil {
|
||||
flag = 0
|
||||
val.Dbuf = self.dbuf
|
||||
val.Dcap = types.MaxDigitNums
|
||||
}
|
||||
self.p = native.Value(sv.Ptr, sv.Len, self.p, &val, uint64(flag))
|
||||
return
|
||||
}
|
||||
|
||||
func (self *Parser) skip() (int, types.ParsingError) {
|
||||
fsm := types.NewStateMachine()
|
||||
start := native.SkipOne(&self.s, &self.p, fsm, 0)
|
||||
types.FreeStateMachine(fsm)
|
||||
|
||||
if start < 0 {
|
||||
return self.p, types.ParsingError(-start)
|
||||
}
|
||||
return start, 0
|
||||
}
|
||||
|
||||
func (self *Node) encodeInterface(buf *[]byte) error {
|
||||
//WARN: NOT compatible with json.Encoder
|
||||
return encoder.EncodeInto(buf, self.packAny(), encoder.NoEncoderNewline)
|
||||
}
|
||||
|
||||
func (self *Parser) skipFast() (int, types.ParsingError) {
|
||||
start := native.SkipOneFast(&self.s, &self.p)
|
||||
if start < 0 {
|
||||
return self.p, types.ParsingError(-start)
|
||||
}
|
||||
return start, 0
|
||||
}
|
||||
|
||||
func (self *Parser) getByPath(validate bool, path ...interface{}) (int, types.ParsingError) {
|
||||
var fsm *types.StateMachine
|
||||
if validate {
|
||||
fsm = types.NewStateMachine()
|
||||
}
|
||||
start := native.GetByPath(&self.s, &self.p, &path, fsm)
|
||||
if validate {
|
||||
types.FreeStateMachine(fsm)
|
||||
}
|
||||
runtime.KeepAlive(path)
|
||||
if start < 0 {
|
||||
return self.p, types.ParsingError(-start)
|
||||
}
|
||||
return start, 0
|
||||
}
|
||||
|
||||
func validate_utf8(str string) bool {
|
||||
return utf8.ValidateString(str)
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
// +build !amd64,!arm64 go1.26 !go1.17 arm64,!go1.20
|
||||
|
||||
/*
|
||||
* Copyright 2022 ByteDance Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ast
|
||||
|
||||
import (
|
||||
`encoding/json`
|
||||
`unicode/utf8`
|
||||
|
||||
`github.com/bytedance/sonic/internal/native/types`
|
||||
`github.com/bytedance/sonic/internal/compat`
|
||||
)
|
||||
|
||||
func init() {
|
||||
compat.Warn("sonic/ast")
|
||||
}
|
||||
|
||||
func quote(buf *[]byte, val string) {
|
||||
quoteString(buf, val)
|
||||
}
|
||||
|
||||
func (self *Parser) decodeValue() (val types.JsonState) {
|
||||
e, v := decodeValue(self.s, self.p, self.dbuf == nil)
|
||||
if e < 0 {
|
||||
return v
|
||||
}
|
||||
self.p = e
|
||||
return v
|
||||
}
|
||||
|
||||
func (self *Parser) skip() (int, types.ParsingError) {
|
||||
e, s := skipValue(self.s, self.p)
|
||||
if e < 0 {
|
||||
return self.p, types.ParsingError(-e)
|
||||
}
|
||||
self.p = e
|
||||
return s, 0
|
||||
}
|
||||
|
||||
func (self *Parser) skipFast() (int, types.ParsingError) {
|
||||
e, s := skipValueFast(self.s, self.p)
|
||||
if e < 0 {
|
||||
return self.p, types.ParsingError(-e)
|
||||
}
|
||||
self.p = e
|
||||
return s, 0
|
||||
}
|
||||
|
||||
func (self *Node) encodeInterface(buf *[]byte) error {
|
||||
out, err := json.Marshal(self.packAny())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*buf = append(*buf, out...)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *Parser) getByPath(validate bool, path ...interface{}) (int, types.ParsingError) {
|
||||
for _, p := range path {
|
||||
if idx, ok := p.(int); ok && idx >= 0 {
|
||||
if err := self.searchIndex(idx); err != 0 {
|
||||
return self.p, err
|
||||
}
|
||||
} else if key, ok := p.(string); ok {
|
||||
if err := self.searchKey(key); err != 0 {
|
||||
return self.p, err
|
||||
}
|
||||
} else {
|
||||
panic("path must be either int(>=0) or string")
|
||||
}
|
||||
}
|
||||
|
||||
var start int
|
||||
var e types.ParsingError
|
||||
if validate {
|
||||
start, e = self.skip()
|
||||
} else {
|
||||
start, e = self.skipFast()
|
||||
}
|
||||
if e != 0 {
|
||||
return self.p, e
|
||||
}
|
||||
return start, 0
|
||||
}
|
||||
|
||||
func validate_utf8(str string) bool {
|
||||
return utf8.ValidString(str)
|
||||
}
|
||||
@@ -0,0 +1,470 @@
|
||||
/**
|
||||
* Copyright 2023 ByteDance Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ast
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"unsafe"
|
||||
|
||||
"github.com/bytedance/sonic/internal/caching"
|
||||
)
|
||||
|
||||
type nodeChunk [_DEFAULT_NODE_CAP]Node
|
||||
|
||||
type linkedNodes struct {
|
||||
head nodeChunk
|
||||
tail []*nodeChunk
|
||||
size int
|
||||
}
|
||||
|
||||
func (self *linkedNodes) Cap() int {
|
||||
if self == nil {
|
||||
return 0
|
||||
}
|
||||
return (len(self.tail)+1)*_DEFAULT_NODE_CAP
|
||||
}
|
||||
|
||||
func (self *linkedNodes) Len() int {
|
||||
if self == nil {
|
||||
return 0
|
||||
}
|
||||
return self.size
|
||||
}
|
||||
|
||||
func (self *linkedNodes) At(i int) (*Node) {
|
||||
if self == nil {
|
||||
return nil
|
||||
}
|
||||
if i >= 0 && i<self.size && i < _DEFAULT_NODE_CAP {
|
||||
return &self.head[i]
|
||||
} else if i >= _DEFAULT_NODE_CAP && i<self.size {
|
||||
a, b := i/_DEFAULT_NODE_CAP-1, i%_DEFAULT_NODE_CAP
|
||||
if a < len(self.tail) {
|
||||
return &self.tail[a][b]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *linkedNodes) MoveOne(source int, target int) {
|
||||
if source == target {
|
||||
return
|
||||
}
|
||||
if source < 0 || source >= self.size || target < 0 || target >= self.size {
|
||||
return
|
||||
}
|
||||
// reserve source
|
||||
n := *self.At(source)
|
||||
if source < target {
|
||||
// move every element (source,target] one step back
|
||||
for i:=source; i<target; i++ {
|
||||
*self.At(i) = *self.At(i+1)
|
||||
}
|
||||
} else {
|
||||
// move every element [target,source) one step forward
|
||||
for i:=source; i>target; i-- {
|
||||
*self.At(i) = *self.At(i-1)
|
||||
}
|
||||
}
|
||||
// set target
|
||||
*self.At(target) = n
|
||||
}
|
||||
|
||||
func (self *linkedNodes) Pop() {
|
||||
if self == nil || self.size == 0 {
|
||||
return
|
||||
}
|
||||
self.Set(self.size-1, Node{})
|
||||
self.size--
|
||||
}
|
||||
|
||||
func (self *linkedNodes) Push(v Node) {
|
||||
self.Set(self.size, v)
|
||||
}
|
||||
|
||||
|
||||
func (self *linkedNodes) Set(i int, v Node) {
|
||||
if i < _DEFAULT_NODE_CAP {
|
||||
self.head[i] = v
|
||||
if self.size <= i {
|
||||
self.size = i+1
|
||||
}
|
||||
return
|
||||
}
|
||||
a, b := i/_DEFAULT_NODE_CAP-1, i%_DEFAULT_NODE_CAP
|
||||
if a < 0 {
|
||||
self.head[b] = v
|
||||
} else {
|
||||
self.growTailLength(a+1)
|
||||
var n = &self.tail[a]
|
||||
if *n == nil {
|
||||
*n = new(nodeChunk)
|
||||
}
|
||||
(*n)[b] = v
|
||||
}
|
||||
if self.size <= i {
|
||||
self.size = i+1
|
||||
}
|
||||
}
|
||||
|
||||
func (self *linkedNodes) growTailLength(l int) {
|
||||
if l <= len(self.tail) {
|
||||
return
|
||||
}
|
||||
c := cap(self.tail)
|
||||
for c < l {
|
||||
c += 1 + c>>_APPEND_GROW_SHIFT
|
||||
}
|
||||
if c == cap(self.tail) {
|
||||
self.tail = self.tail[:l]
|
||||
return
|
||||
}
|
||||
tmp := make([]*nodeChunk, l, c)
|
||||
copy(tmp, self.tail)
|
||||
self.tail = tmp
|
||||
}
|
||||
|
||||
func (self *linkedNodes) ToSlice(con []Node) {
|
||||
if len(con) < self.size {
|
||||
return
|
||||
}
|
||||
i := (self.size-1)
|
||||
a, b := i/_DEFAULT_NODE_CAP-1, i%_DEFAULT_NODE_CAP
|
||||
if a < 0 {
|
||||
copy(con, self.head[:b+1])
|
||||
return
|
||||
} else {
|
||||
copy(con, self.head[:])
|
||||
con = con[_DEFAULT_NODE_CAP:]
|
||||
}
|
||||
|
||||
for i:=0; i<a; i++ {
|
||||
copy(con, self.tail[i][:])
|
||||
con = con[_DEFAULT_NODE_CAP:]
|
||||
}
|
||||
copy(con, self.tail[a][:b+1])
|
||||
}
|
||||
|
||||
func (self *linkedNodes) FromSlice(con []Node) {
|
||||
self.size = len(con)
|
||||
i := self.size-1
|
||||
a, b := i/_DEFAULT_NODE_CAP-1, i%_DEFAULT_NODE_CAP
|
||||
if a < 0 {
|
||||
copy(self.head[:b+1], con)
|
||||
return
|
||||
} else {
|
||||
copy(self.head[:], con)
|
||||
con = con[_DEFAULT_NODE_CAP:]
|
||||
}
|
||||
|
||||
if cap(self.tail) <= a {
|
||||
c := (a+1) + (a+1)>>_APPEND_GROW_SHIFT
|
||||
self.tail = make([]*nodeChunk, a+1, c)
|
||||
}
|
||||
self.tail = self.tail[:a+1]
|
||||
|
||||
for i:=0; i<a; i++ {
|
||||
self.tail[i] = new(nodeChunk)
|
||||
copy(self.tail[i][:], con)
|
||||
con = con[_DEFAULT_NODE_CAP:]
|
||||
}
|
||||
|
||||
self.tail[a] = new(nodeChunk)
|
||||
copy(self.tail[a][:b+1], con)
|
||||
}
|
||||
|
||||
type pairChunk [_DEFAULT_NODE_CAP]Pair
|
||||
|
||||
type linkedPairs struct {
|
||||
index map[uint64]int
|
||||
head pairChunk
|
||||
tail []*pairChunk
|
||||
size int
|
||||
}
|
||||
|
||||
func (self *linkedPairs) BuildIndex() {
|
||||
if self.index == nil {
|
||||
self.index = make(map[uint64]int, self.size)
|
||||
}
|
||||
for i:=0; i<self.size; i++ {
|
||||
p := self.At(i)
|
||||
self.index[p.hash] = i
|
||||
}
|
||||
}
|
||||
|
||||
func (self *linkedPairs) Cap() int {
|
||||
if self == nil {
|
||||
return 0
|
||||
}
|
||||
return (len(self.tail)+1)*_DEFAULT_NODE_CAP
|
||||
}
|
||||
|
||||
func (self *linkedPairs) Len() int {
|
||||
if self == nil {
|
||||
return 0
|
||||
}
|
||||
return self.size
|
||||
}
|
||||
|
||||
func (self *linkedPairs) At(i int) *Pair {
|
||||
if self == nil {
|
||||
return nil
|
||||
}
|
||||
if i >= 0 && i < _DEFAULT_NODE_CAP && i<self.size {
|
||||
return &self.head[i]
|
||||
} else if i >= _DEFAULT_NODE_CAP && i<self.size {
|
||||
a, b := i/_DEFAULT_NODE_CAP-1, i%_DEFAULT_NODE_CAP
|
||||
if a < len(self.tail) {
|
||||
return &self.tail[a][b]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *linkedPairs) Push(v Pair) {
|
||||
self.Set(self.size, v)
|
||||
}
|
||||
|
||||
func (self *linkedPairs) Pop() {
|
||||
if self == nil || self.size == 0 {
|
||||
return
|
||||
}
|
||||
self.Unset(self.size-1)
|
||||
self.size--
|
||||
}
|
||||
|
||||
func (self *linkedPairs) Unset(i int) {
|
||||
if self.index != nil {
|
||||
p := self.At(i)
|
||||
delete(self.index, p.hash)
|
||||
}
|
||||
self.set(i, Pair{})
|
||||
}
|
||||
|
||||
func (self *linkedPairs) Set(i int, v Pair) {
|
||||
if self.index != nil {
|
||||
h := v.hash
|
||||
self.index[h] = i
|
||||
}
|
||||
self.set(i, v)
|
||||
}
|
||||
|
||||
func (self *linkedPairs) set(i int, v Pair) {
|
||||
if i < _DEFAULT_NODE_CAP {
|
||||
self.head[i] = v
|
||||
if self.size <= i {
|
||||
self.size = i+1
|
||||
}
|
||||
return
|
||||
}
|
||||
a, b := i/_DEFAULT_NODE_CAP-1, i%_DEFAULT_NODE_CAP
|
||||
if a < 0 {
|
||||
self.head[b] = v
|
||||
} else {
|
||||
self.growTailLength(a+1)
|
||||
var n = &self.tail[a]
|
||||
if *n == nil {
|
||||
*n = new(pairChunk)
|
||||
}
|
||||
(*n)[b] = v
|
||||
}
|
||||
if self.size <= i {
|
||||
self.size = i+1
|
||||
}
|
||||
}
|
||||
|
||||
func (self *linkedPairs) growTailLength(l int) {
|
||||
if l <= len(self.tail) {
|
||||
return
|
||||
}
|
||||
c := cap(self.tail)
|
||||
for c < l {
|
||||
c += 1 + c>>_APPEND_GROW_SHIFT
|
||||
}
|
||||
if c == cap(self.tail) {
|
||||
self.tail = self.tail[:l]
|
||||
return
|
||||
}
|
||||
tmp := make([]*pairChunk, l, c)
|
||||
copy(tmp, self.tail)
|
||||
self.tail = tmp
|
||||
}
|
||||
|
||||
// linear search
|
||||
func (self *linkedPairs) Get(key string) (*Pair, int) {
|
||||
if self.index != nil {
|
||||
// fast-path
|
||||
i, ok := self.index[caching.StrHash(key)]
|
||||
if ok {
|
||||
n := self.At(i)
|
||||
if n.Key == key {
|
||||
return n, i
|
||||
}
|
||||
// hash conflicts
|
||||
goto linear_search
|
||||
} else {
|
||||
return nil, -1
|
||||
}
|
||||
}
|
||||
linear_search:
|
||||
for i:=0; i<self.size; i++ {
|
||||
if n := self.At(i); n.Key == key {
|
||||
return n, i
|
||||
}
|
||||
}
|
||||
return nil, -1
|
||||
}
|
||||
|
||||
func (self *linkedPairs) ToSlice(con []Pair) {
|
||||
if len(con) < self.size {
|
||||
return
|
||||
}
|
||||
i := self.size-1
|
||||
a, b := i/_DEFAULT_NODE_CAP-1, i%_DEFAULT_NODE_CAP
|
||||
|
||||
if a < 0 {
|
||||
copy(con, self.head[:b+1])
|
||||
return
|
||||
} else {
|
||||
copy(con, self.head[:])
|
||||
con = con[_DEFAULT_NODE_CAP:]
|
||||
}
|
||||
|
||||
for i:=0; i<a; i++ {
|
||||
copy(con, self.tail[i][:])
|
||||
con = con[_DEFAULT_NODE_CAP:]
|
||||
}
|
||||
copy(con, self.tail[a][:b+1])
|
||||
}
|
||||
|
||||
func (self *linkedPairs) ToMap(con map[string]Node) {
|
||||
for i:=0; i<self.size; i++ {
|
||||
n := self.At(i)
|
||||
con[n.Key] = n.Value
|
||||
}
|
||||
}
|
||||
|
||||
func (self *linkedPairs) copyPairs(to []Pair, from []Pair, l int) {
|
||||
copy(to, from)
|
||||
if self.index != nil {
|
||||
for i:=0; i<l; i++ {
|
||||
// NOTICE: in case of user not pass hash, just cal it
|
||||
h := caching.StrHash(from[i].Key)
|
||||
from[i].hash = h
|
||||
self.index[h] = i
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (self *linkedPairs) FromSlice(con []Pair) {
|
||||
self.size = len(con)
|
||||
i := self.size-1
|
||||
a, b := i/_DEFAULT_NODE_CAP-1, i%_DEFAULT_NODE_CAP
|
||||
if a < 0 {
|
||||
self.copyPairs(self.head[:b+1], con, b+1)
|
||||
return
|
||||
} else {
|
||||
self.copyPairs(self.head[:], con, len(self.head))
|
||||
con = con[_DEFAULT_NODE_CAP:]
|
||||
}
|
||||
|
||||
if cap(self.tail) <= a {
|
||||
c := (a+1) + (a+1)>>_APPEND_GROW_SHIFT
|
||||
self.tail = make([]*pairChunk, a+1, c)
|
||||
}
|
||||
self.tail = self.tail[:a+1]
|
||||
|
||||
for i:=0; i<a; i++ {
|
||||
self.tail[i] = new(pairChunk)
|
||||
self.copyPairs(self.tail[i][:], con, len(self.tail[i]))
|
||||
con = con[_DEFAULT_NODE_CAP:]
|
||||
}
|
||||
|
||||
self.tail[a] = new(pairChunk)
|
||||
self.copyPairs(self.tail[a][:b+1], con, b+1)
|
||||
}
|
||||
|
||||
func (self *linkedPairs) Less(i, j int) bool {
|
||||
return lessFrom(self.At(i).Key, self.At(j).Key, 0)
|
||||
}
|
||||
|
||||
func (self *linkedPairs) Swap(i, j int) {
|
||||
a, b := self.At(i), self.At(j)
|
||||
if self.index != nil {
|
||||
self.index[a.hash] = j
|
||||
self.index[b.hash] = i
|
||||
}
|
||||
*a, *b = *b, *a
|
||||
}
|
||||
|
||||
func (self *linkedPairs) Sort() {
|
||||
sort.Stable(self)
|
||||
}
|
||||
|
||||
// Compare two strings from the pos d.
|
||||
func lessFrom(a, b string, d int) bool {
|
||||
l := len(a)
|
||||
if l > len(b) {
|
||||
l = len(b)
|
||||
}
|
||||
for i := d; i < l; i++ {
|
||||
if a[i] == b[i] {
|
||||
continue
|
||||
}
|
||||
return a[i] < b[i]
|
||||
}
|
||||
return len(a) < len(b)
|
||||
}
|
||||
|
||||
type parseObjectStack struct {
|
||||
parser Parser
|
||||
v linkedPairs
|
||||
}
|
||||
|
||||
type parseArrayStack struct {
|
||||
parser Parser
|
||||
v linkedNodes
|
||||
}
|
||||
|
||||
func newLazyArray(p *Parser) Node {
|
||||
s := new(parseArrayStack)
|
||||
s.parser = *p
|
||||
return Node{
|
||||
t: _V_ARRAY_LAZY,
|
||||
p: unsafe.Pointer(s),
|
||||
}
|
||||
}
|
||||
|
||||
func newLazyObject(p *Parser) Node {
|
||||
s := new(parseObjectStack)
|
||||
s.parser = *p
|
||||
return Node{
|
||||
t: _V_OBJECT_LAZY,
|
||||
p: unsafe.Pointer(s),
|
||||
}
|
||||
}
|
||||
|
||||
func (self *Node) getParserAndArrayStack() (*Parser, *parseArrayStack) {
|
||||
stack := (*parseArrayStack)(self.p)
|
||||
return &stack.parser, stack
|
||||
}
|
||||
|
||||
func (self *Node) getParserAndObjectStack() (*Parser, *parseObjectStack) {
|
||||
stack := (*parseObjectStack)(self.p)
|
||||
return &stack.parser, stack
|
||||
}
|
||||
|
||||
@@ -0,0 +1,557 @@
|
||||
/*
|
||||
* Copyright 2022 ByteDance Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ast
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"unsafe"
|
||||
|
||||
"github.com/bytedance/sonic/internal/native/types"
|
||||
"github.com/bytedance/sonic/internal/rt"
|
||||
"github.com/bytedance/sonic/internal/utils"
|
||||
"github.com/bytedance/sonic/unquote"
|
||||
)
|
||||
|
||||
|
||||
var bytesNull = []byte("null")
|
||||
|
||||
const (
|
||||
strNull = "null"
|
||||
bytesTrue = "true"
|
||||
bytesFalse = "false"
|
||||
bytesObject = "{}"
|
||||
bytesArray = "[]"
|
||||
)
|
||||
|
||||
//go:nocheckptr
|
||||
func skipBlank(src string, pos int) int {
|
||||
se := uintptr(rt.IndexChar(src, len(src)))
|
||||
sp := uintptr(rt.IndexChar(src, pos))
|
||||
|
||||
for sp < se {
|
||||
if !utils.IsSpace(*(*byte)(unsafe.Pointer(sp))) {
|
||||
break
|
||||
}
|
||||
sp += 1
|
||||
}
|
||||
if sp >= se {
|
||||
return -int(types.ERR_EOF)
|
||||
}
|
||||
runtime.KeepAlive(src)
|
||||
return int(sp - uintptr(rt.IndexChar(src, 0)))
|
||||
}
|
||||
|
||||
func decodeNull(src string, pos int) (ret int) {
|
||||
ret = pos + 4
|
||||
if ret > len(src) {
|
||||
return -int(types.ERR_EOF)
|
||||
}
|
||||
if src[pos:ret] == strNull {
|
||||
return ret
|
||||
} else {
|
||||
return -int(types.ERR_INVALID_CHAR)
|
||||
}
|
||||
}
|
||||
|
||||
func decodeTrue(src string, pos int) (ret int) {
|
||||
ret = pos + 4
|
||||
if ret > len(src) {
|
||||
return -int(types.ERR_EOF)
|
||||
}
|
||||
if src[pos:ret] == bytesTrue {
|
||||
return ret
|
||||
} else {
|
||||
return -int(types.ERR_INVALID_CHAR)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func decodeFalse(src string, pos int) (ret int) {
|
||||
ret = pos + 5
|
||||
if ret > len(src) {
|
||||
return -int(types.ERR_EOF)
|
||||
}
|
||||
if src[pos:ret] == bytesFalse {
|
||||
return ret
|
||||
}
|
||||
return -int(types.ERR_INVALID_CHAR)
|
||||
}
|
||||
|
||||
//go:nocheckptr
|
||||
func decodeString(src string, pos int) (ret int, v string) {
|
||||
ret, ep := skipString(src, pos)
|
||||
if ep == -1 {
|
||||
(*rt.GoString)(unsafe.Pointer(&v)).Ptr = rt.IndexChar(src, pos+1)
|
||||
(*rt.GoString)(unsafe.Pointer(&v)).Len = ret - pos - 2
|
||||
return ret, v
|
||||
}
|
||||
|
||||
result, err := unquote.String(src[pos:ret])
|
||||
if err != 0 {
|
||||
return -int(types.ERR_INVALID_CHAR), ""
|
||||
}
|
||||
|
||||
runtime.KeepAlive(src)
|
||||
return ret, result
|
||||
}
|
||||
|
||||
func decodeBinary(src string, pos int) (ret int, v []byte) {
|
||||
var vv string
|
||||
ret, vv = decodeString(src, pos)
|
||||
if ret < 0 {
|
||||
return ret, nil
|
||||
}
|
||||
var err error
|
||||
v, err = base64.StdEncoding.DecodeString(vv)
|
||||
if err != nil {
|
||||
return -int(types.ERR_INVALID_CHAR), nil
|
||||
}
|
||||
return ret, v
|
||||
}
|
||||
|
||||
func isDigit(c byte) bool {
|
||||
return c >= '0' && c <= '9'
|
||||
}
|
||||
|
||||
//go:nocheckptr
|
||||
func decodeInt64(src string, pos int) (ret int, v int64, err error) {
|
||||
sp := uintptr(rt.IndexChar(src, pos))
|
||||
ss := uintptr(sp)
|
||||
se := uintptr(rt.IndexChar(src, len(src)))
|
||||
if uintptr(sp) >= se {
|
||||
return -int(types.ERR_EOF), 0, nil
|
||||
}
|
||||
|
||||
if c := *(*byte)(unsafe.Pointer(sp)); c == '-' {
|
||||
sp += 1
|
||||
}
|
||||
if sp == se {
|
||||
return -int(types.ERR_EOF), 0, nil
|
||||
}
|
||||
|
||||
for ; sp < se; sp += uintptr(1) {
|
||||
if !isDigit(*(*byte)(unsafe.Pointer(sp))) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if sp < se {
|
||||
if c := *(*byte)(unsafe.Pointer(sp)); c == '.' || c == 'e' || c == 'E' {
|
||||
return -int(types.ERR_INVALID_NUMBER_FMT), 0, nil
|
||||
}
|
||||
}
|
||||
|
||||
var vv string
|
||||
ret = int(uintptr(sp) - uintptr((*rt.GoString)(unsafe.Pointer(&src)).Ptr))
|
||||
(*rt.GoString)(unsafe.Pointer(&vv)).Ptr = unsafe.Pointer(ss)
|
||||
(*rt.GoString)(unsafe.Pointer(&vv)).Len = ret - pos
|
||||
|
||||
v, err = strconv.ParseInt(vv, 10, 64)
|
||||
if err != nil {
|
||||
//NOTICE: allow overflow here
|
||||
if err.(*strconv.NumError).Err == strconv.ErrRange {
|
||||
return ret, 0, err
|
||||
}
|
||||
return -int(types.ERR_INVALID_CHAR), 0, err
|
||||
}
|
||||
|
||||
runtime.KeepAlive(src)
|
||||
return ret, v, nil
|
||||
}
|
||||
|
||||
func isNumberChars(c byte) bool {
|
||||
return (c >= '0' && c <= '9') || c == '+' || c == '-' || c == 'e' || c == 'E' || c == '.'
|
||||
}
|
||||
|
||||
//go:nocheckptr
|
||||
func decodeFloat64(src string, pos int) (ret int, v float64, err error) {
|
||||
sp := uintptr(rt.IndexChar(src, pos))
|
||||
ss := uintptr(sp)
|
||||
se := uintptr(rt.IndexChar(src, len(src)))
|
||||
if uintptr(sp) >= se {
|
||||
return -int(types.ERR_EOF), 0, nil
|
||||
}
|
||||
|
||||
if c := *(*byte)(unsafe.Pointer(sp)); c == '-' {
|
||||
sp += 1
|
||||
}
|
||||
if sp == se {
|
||||
return -int(types.ERR_EOF), 0, nil
|
||||
}
|
||||
|
||||
for ; sp < se; sp += uintptr(1) {
|
||||
if !isNumberChars(*(*byte)(unsafe.Pointer(sp))) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
var vv string
|
||||
ret = int(uintptr(sp) - uintptr((*rt.GoString)(unsafe.Pointer(&src)).Ptr))
|
||||
(*rt.GoString)(unsafe.Pointer(&vv)).Ptr = unsafe.Pointer(ss)
|
||||
(*rt.GoString)(unsafe.Pointer(&vv)).Len = ret - pos
|
||||
|
||||
v, err = strconv.ParseFloat(vv, 64)
|
||||
if err != nil {
|
||||
//NOTICE: allow overflow here
|
||||
if err.(*strconv.NumError).Err == strconv.ErrRange {
|
||||
return ret, 0, err
|
||||
}
|
||||
return -int(types.ERR_INVALID_CHAR), 0, err
|
||||
}
|
||||
|
||||
runtime.KeepAlive(src)
|
||||
return ret, v, nil
|
||||
}
|
||||
|
||||
func decodeValue(src string, pos int, skipnum bool) (ret int, v types.JsonState) {
|
||||
pos = skipBlank(src, pos)
|
||||
if pos < 0 {
|
||||
return pos, types.JsonState{Vt: types.ValueType(pos)}
|
||||
}
|
||||
switch c := src[pos]; c {
|
||||
case 'n':
|
||||
ret = decodeNull(src, pos)
|
||||
if ret < 0 {
|
||||
return ret, types.JsonState{Vt: types.ValueType(ret)}
|
||||
}
|
||||
return ret, types.JsonState{Vt: types.V_NULL}
|
||||
case '"':
|
||||
var ep int
|
||||
ret, ep = skipString(src, pos)
|
||||
if ret < 0 {
|
||||
return ret, types.JsonState{Vt: types.ValueType(ret)}
|
||||
}
|
||||
return ret, types.JsonState{Vt: types.V_STRING, Iv: int64(pos + 1), Ep: ep}
|
||||
case '{':
|
||||
return pos + 1, types.JsonState{Vt: types.V_OBJECT}
|
||||
case '[':
|
||||
return pos + 1, types.JsonState{Vt: types.V_ARRAY}
|
||||
case 't':
|
||||
ret = decodeTrue(src, pos)
|
||||
if ret < 0 {
|
||||
return ret, types.JsonState{Vt: types.ValueType(ret)}
|
||||
}
|
||||
return ret, types.JsonState{Vt: types.V_TRUE}
|
||||
case 'f':
|
||||
ret = decodeFalse(src, pos)
|
||||
if ret < 0 {
|
||||
return ret, types.JsonState{Vt: types.ValueType(ret)}
|
||||
}
|
||||
return ret, types.JsonState{Vt: types.V_FALSE}
|
||||
case '-', '+', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
|
||||
if skipnum {
|
||||
ret = skipNumber(src, pos)
|
||||
if ret >= 0 {
|
||||
return ret, types.JsonState{Vt: types.V_DOUBLE, Iv: 0, Ep: pos}
|
||||
} else {
|
||||
return ret, types.JsonState{Vt: types.ValueType(ret)}
|
||||
}
|
||||
} else {
|
||||
var iv int64
|
||||
ret, iv, _ = decodeInt64(src, pos)
|
||||
if ret >= 0 {
|
||||
return ret, types.JsonState{Vt: types.V_INTEGER, Iv: iv, Ep: pos}
|
||||
} else if ret != -int(types.ERR_INVALID_NUMBER_FMT) {
|
||||
return ret, types.JsonState{Vt: types.ValueType(ret)}
|
||||
}
|
||||
var fv float64
|
||||
ret, fv, _ = decodeFloat64(src, pos)
|
||||
if ret >= 0 {
|
||||
return ret, types.JsonState{Vt: types.V_DOUBLE, Dv: fv, Ep: pos}
|
||||
} else {
|
||||
return ret, types.JsonState{Vt: types.ValueType(ret)}
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
return -int(types.ERR_INVALID_CHAR), types.JsonState{Vt:-types.ValueType(types.ERR_INVALID_CHAR)}
|
||||
}
|
||||
}
|
||||
|
||||
//go:nocheckptr
|
||||
func skipNumber(src string, pos int) (ret int) {
|
||||
return utils.SkipNumber(src, pos)
|
||||
}
|
||||
|
||||
//go:nocheckptr
|
||||
func skipString(src string, pos int) (ret int, ep int) {
|
||||
if pos+1 >= len(src) {
|
||||
return -int(types.ERR_EOF), -1
|
||||
}
|
||||
|
||||
sp := uintptr(rt.IndexChar(src, pos))
|
||||
se := uintptr(rt.IndexChar(src, len(src)))
|
||||
|
||||
// not start with quote
|
||||
if *(*byte)(unsafe.Pointer(sp)) != '"' {
|
||||
return -int(types.ERR_INVALID_CHAR), -1
|
||||
}
|
||||
sp += 1
|
||||
|
||||
ep = -1
|
||||
for sp < se {
|
||||
c := *(*byte)(unsafe.Pointer(sp))
|
||||
if c == '\\' {
|
||||
if ep == -1 {
|
||||
ep = int(uintptr(sp) - uintptr((*rt.GoString)(unsafe.Pointer(&src)).Ptr))
|
||||
}
|
||||
sp += 2
|
||||
continue
|
||||
}
|
||||
sp += 1
|
||||
if c == '"' {
|
||||
return int(uintptr(sp) - uintptr((*rt.GoString)(unsafe.Pointer(&src)).Ptr)), ep
|
||||
}
|
||||
}
|
||||
|
||||
runtime.KeepAlive(src)
|
||||
// not found the closed quote until EOF
|
||||
return -int(types.ERR_EOF), -1
|
||||
}
|
||||
|
||||
//go:nocheckptr
|
||||
func skipPair(src string, pos int, lchar byte, rchar byte) (ret int) {
|
||||
if pos+1 >= len(src) {
|
||||
return -int(types.ERR_EOF)
|
||||
}
|
||||
|
||||
sp := uintptr(rt.IndexChar(src, pos))
|
||||
se := uintptr(rt.IndexChar(src, len(src)))
|
||||
|
||||
if *(*byte)(unsafe.Pointer(sp)) != lchar {
|
||||
return -int(types.ERR_INVALID_CHAR)
|
||||
}
|
||||
|
||||
sp += 1
|
||||
nbrace := 1
|
||||
inquote := false
|
||||
|
||||
for sp < se {
|
||||
c := *(*byte)(unsafe.Pointer(sp))
|
||||
if c == '\\' {
|
||||
sp += 2
|
||||
continue
|
||||
} else if c == '"' {
|
||||
inquote = !inquote
|
||||
} else if c == lchar {
|
||||
if !inquote {
|
||||
nbrace += 1
|
||||
}
|
||||
} else if c == rchar {
|
||||
if !inquote {
|
||||
nbrace -= 1
|
||||
if nbrace == 0 {
|
||||
sp += 1
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
sp += 1
|
||||
}
|
||||
|
||||
if nbrace != 0 {
|
||||
return -int(types.ERR_INVALID_CHAR)
|
||||
}
|
||||
|
||||
runtime.KeepAlive(src)
|
||||
return int(uintptr(sp) - uintptr((*rt.GoString)(unsafe.Pointer(&src)).Ptr))
|
||||
}
|
||||
|
||||
func skipValueFast(src string, pos int) (ret int, start int) {
|
||||
pos = skipBlank(src, pos)
|
||||
if pos < 0 {
|
||||
return pos, -1
|
||||
}
|
||||
switch c := src[pos]; c {
|
||||
case 'n':
|
||||
ret = decodeNull(src, pos)
|
||||
case '"':
|
||||
ret, _ = skipString(src, pos)
|
||||
case '{':
|
||||
ret = skipPair(src, pos, '{', '}')
|
||||
case '[':
|
||||
ret = skipPair(src, pos, '[', ']')
|
||||
case 't':
|
||||
ret = decodeTrue(src, pos)
|
||||
case 'f':
|
||||
ret = decodeFalse(src, pos)
|
||||
case '-', '+', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
|
||||
ret = skipNumber(src, pos)
|
||||
default:
|
||||
ret = -int(types.ERR_INVALID_CHAR)
|
||||
}
|
||||
return ret, pos
|
||||
}
|
||||
|
||||
func skipValue(src string, pos int) (ret int, start int) {
|
||||
pos = skipBlank(src, pos)
|
||||
if pos < 0 {
|
||||
return pos, -1
|
||||
}
|
||||
switch c := src[pos]; c {
|
||||
case 'n':
|
||||
ret = decodeNull(src, pos)
|
||||
case '"':
|
||||
ret, _ = skipString(src, pos)
|
||||
case '{':
|
||||
ret, _ = skipObject(src, pos)
|
||||
case '[':
|
||||
ret, _ = skipArray(src, pos)
|
||||
case 't':
|
||||
ret = decodeTrue(src, pos)
|
||||
case 'f':
|
||||
ret = decodeFalse(src, pos)
|
||||
case '-', '+', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
|
||||
ret = skipNumber(src, pos)
|
||||
default:
|
||||
ret = -int(types.ERR_INVALID_CHAR)
|
||||
}
|
||||
return ret, pos
|
||||
}
|
||||
|
||||
func skipObject(src string, pos int) (ret int, start int) {
|
||||
start = skipBlank(src, pos)
|
||||
if start < 0 {
|
||||
return start, -1
|
||||
}
|
||||
|
||||
if src[start] != '{' {
|
||||
return -int(types.ERR_INVALID_CHAR), -1
|
||||
}
|
||||
|
||||
pos = start + 1
|
||||
pos = skipBlank(src, pos)
|
||||
if pos < 0 {
|
||||
return pos, -1
|
||||
}
|
||||
if src[pos] == '}' {
|
||||
return pos + 1, start
|
||||
}
|
||||
|
||||
for {
|
||||
pos, _ = skipString(src, pos)
|
||||
if pos < 0 {
|
||||
return pos, -1
|
||||
}
|
||||
|
||||
pos = skipBlank(src, pos)
|
||||
if pos < 0 {
|
||||
return pos, -1
|
||||
}
|
||||
if src[pos] != ':' {
|
||||
return -int(types.ERR_INVALID_CHAR), -1
|
||||
}
|
||||
|
||||
pos++
|
||||
pos, _ = skipValue(src, pos)
|
||||
if pos < 0 {
|
||||
return pos, -1
|
||||
}
|
||||
|
||||
pos = skipBlank(src, pos)
|
||||
if pos < 0 {
|
||||
return pos, -1
|
||||
}
|
||||
if src[pos] == '}' {
|
||||
return pos + 1, start
|
||||
}
|
||||
if src[pos] != ',' {
|
||||
return -int(types.ERR_INVALID_CHAR), -1
|
||||
}
|
||||
|
||||
pos++
|
||||
pos = skipBlank(src, pos)
|
||||
if pos < 0 {
|
||||
return pos, -1
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func skipArray(src string, pos int) (ret int, start int) {
|
||||
start = skipBlank(src, pos)
|
||||
if start < 0 {
|
||||
return start, -1
|
||||
}
|
||||
|
||||
if src[start] != '[' {
|
||||
return -int(types.ERR_INVALID_CHAR), -1
|
||||
}
|
||||
|
||||
pos = start + 1
|
||||
pos = skipBlank(src, pos)
|
||||
if pos < 0 {
|
||||
return pos, -1
|
||||
}
|
||||
if src[pos] == ']' {
|
||||
return pos + 1, start
|
||||
}
|
||||
|
||||
for {
|
||||
pos, _ = skipValue(src, pos)
|
||||
if pos < 0 {
|
||||
return pos, -1
|
||||
}
|
||||
|
||||
pos = skipBlank(src, pos)
|
||||
if pos < 0 {
|
||||
return pos, -1
|
||||
}
|
||||
if src[pos] == ']' {
|
||||
return pos + 1, start
|
||||
}
|
||||
if src[pos] != ',' {
|
||||
return -int(types.ERR_INVALID_CHAR), -1
|
||||
}
|
||||
pos++
|
||||
}
|
||||
}
|
||||
|
||||
// DecodeString decodes a JSON string from pos and return golang string.
|
||||
// - needEsc indicates if to unescaped escaping chars
|
||||
// - hasEsc tells if the returned string has escaping chars
|
||||
// - validStr enables validating UTF8 charset
|
||||
//
|
||||
func _DecodeString(src string, pos int, needEsc bool, validStr bool) (v string, ret int, hasEsc bool) {
|
||||
p := NewParserObj(src)
|
||||
p.p = pos
|
||||
switch val := p.decodeValue(); val.Vt {
|
||||
case types.V_STRING:
|
||||
str := p.s[val.Iv : p.p-1]
|
||||
if validStr && !validate_utf8(str) {
|
||||
return "", -int(types.ERR_INVALID_UTF8), false
|
||||
}
|
||||
/* fast path: no escape sequence */
|
||||
if val.Ep == -1 {
|
||||
return str, p.p, false
|
||||
} else if !needEsc {
|
||||
return str, p.p, true
|
||||
}
|
||||
/* unquote the string */
|
||||
out, err := unquote.String(str)
|
||||
/* check for errors */
|
||||
if err != 0 {
|
||||
return "", -int(err), true
|
||||
} else {
|
||||
return out, p.p, true
|
||||
}
|
||||
default:
|
||||
return "", -int(_ERR_UNSUPPORT_TYPE), false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
/*
|
||||
* Copyright 2021 ByteDance Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ast
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/bytedance/gopkg/lang/dirtmake"
|
||||
"github.com/bytedance/sonic/internal/rt"
|
||||
"github.com/bytedance/sonic/option"
|
||||
)
|
||||
|
||||
func quoteString(e *[]byte, s string) {
|
||||
*e = append(*e, '"')
|
||||
start := 0
|
||||
for i := 0; i < len(s); {
|
||||
if b := s[i]; b < utf8.RuneSelf {
|
||||
if rt.SafeSet[b] {
|
||||
i++
|
||||
continue
|
||||
}
|
||||
if start < i {
|
||||
*e = append(*e, s[start:i]...)
|
||||
}
|
||||
*e = append(*e, '\\')
|
||||
switch b {
|
||||
case '\\', '"':
|
||||
*e = append(*e, b)
|
||||
case '\n':
|
||||
*e = append(*e, 'n')
|
||||
case '\r':
|
||||
*e = append(*e, 'r')
|
||||
case '\t':
|
||||
*e = append(*e, 't')
|
||||
default:
|
||||
// This encodes bytes < 0x20 except for \t, \n and \r.
|
||||
// If escapeHTML is set, it also escapes <, >, and &
|
||||
// because they can lead to security holes when
|
||||
// user-controlled strings are rendered into JSON
|
||||
// and served to some browsers.
|
||||
*e = append(*e, `u00`...)
|
||||
*e = append(*e, rt.Hex[b>>4])
|
||||
*e = append(*e, rt.Hex[b&0xF])
|
||||
}
|
||||
i++
|
||||
start = i
|
||||
continue
|
||||
}
|
||||
c, size := utf8.DecodeRuneInString(s[i:])
|
||||
// if c == utf8.RuneError && size == 1 {
|
||||
// if start < i {
|
||||
// e.Write(s[start:i])
|
||||
// }
|
||||
// e.WriteString(`\ufffd`)
|
||||
// i += size
|
||||
// start = i
|
||||
// continue
|
||||
// }
|
||||
if c == '\u2028' || c == '\u2029' {
|
||||
if start < i {
|
||||
*e = append(*e, s[start:i]...)
|
||||
}
|
||||
*e = append(*e, `\u202`...)
|
||||
*e = append(*e, rt.Hex[c&0xF])
|
||||
i += size
|
||||
start = i
|
||||
continue
|
||||
}
|
||||
i += size
|
||||
}
|
||||
if start < len(s) {
|
||||
*e = append(*e, s[start:]...)
|
||||
}
|
||||
*e = append(*e, '"')
|
||||
}
|
||||
|
||||
var bytesPool = sync.Pool{}
|
||||
|
||||
func (self *Node) MarshalJSON() ([]byte, error) {
|
||||
if self == nil {
|
||||
return bytesNull, nil
|
||||
}
|
||||
|
||||
// fast path for raw node
|
||||
if self.isRaw() {
|
||||
return rt.Str2Mem(self.toString()), nil
|
||||
}
|
||||
|
||||
buf := newBuffer()
|
||||
err := self.encode(buf)
|
||||
if err != nil {
|
||||
freeBuffer(buf)
|
||||
return nil, err
|
||||
}
|
||||
var ret []byte
|
||||
if !rt.CanSizeResue(cap(*buf)) {
|
||||
ret = *buf
|
||||
} else {
|
||||
ret = dirtmake.Bytes(len(*buf), len(*buf))
|
||||
copy(ret, *buf)
|
||||
freeBuffer(buf)
|
||||
}
|
||||
return ret, err
|
||||
}
|
||||
|
||||
func newBuffer() *[]byte {
|
||||
if ret := bytesPool.Get(); ret != nil {
|
||||
return ret.(*[]byte)
|
||||
} else {
|
||||
buf := make([]byte, 0, option.DefaultAstBufferSize)
|
||||
return &buf
|
||||
}
|
||||
}
|
||||
|
||||
func freeBuffer(buf *[]byte) {
|
||||
if !rt.CanSizeResue(cap(*buf)) {
|
||||
return
|
||||
}
|
||||
*buf = (*buf)[:0]
|
||||
bytesPool.Put(buf)
|
||||
}
|
||||
|
||||
func (self *Node) encode(buf *[]byte) error {
|
||||
if self.isRaw() {
|
||||
return self.encodeRaw(buf)
|
||||
}
|
||||
switch int(self.itype()) {
|
||||
case V_NONE : return ErrNotExist
|
||||
case V_ERROR : return self.Check()
|
||||
case V_NULL : return self.encodeNull(buf)
|
||||
case V_TRUE : return self.encodeTrue(buf)
|
||||
case V_FALSE : return self.encodeFalse(buf)
|
||||
case V_ARRAY : return self.encodeArray(buf)
|
||||
case V_OBJECT: return self.encodeObject(buf)
|
||||
case V_STRING: return self.encodeString(buf)
|
||||
case V_NUMBER: return self.encodeNumber(buf)
|
||||
case V_ANY : return self.encodeInterface(buf)
|
||||
default : return ErrUnsupportType
|
||||
}
|
||||
}
|
||||
|
||||
func (self *Node) encodeRaw(buf *[]byte) error {
|
||||
lock := self.rlock()
|
||||
if !self.isRaw() {
|
||||
self.runlock()
|
||||
return self.encode(buf)
|
||||
}
|
||||
raw := self.toString()
|
||||
if lock {
|
||||
self.runlock()
|
||||
}
|
||||
*buf = append(*buf, raw...)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *Node) encodeNull(buf *[]byte) error {
|
||||
*buf = append(*buf, strNull...)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *Node) encodeTrue(buf *[]byte) error {
|
||||
*buf = append(*buf, bytesTrue...)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *Node) encodeFalse(buf *[]byte) error {
|
||||
*buf = append(*buf, bytesFalse...)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *Node) encodeNumber(buf *[]byte) error {
|
||||
str := self.toString()
|
||||
*buf = append(*buf, str...)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *Node) encodeString(buf *[]byte) error {
|
||||
if self.l == 0 {
|
||||
*buf = append(*buf, '"', '"')
|
||||
return nil
|
||||
}
|
||||
|
||||
quote(buf, self.toString())
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *Node) encodeArray(buf *[]byte) error {
|
||||
if self.isLazy() {
|
||||
if err := self.skipAllIndex(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
nb := self.len()
|
||||
if nb == 0 {
|
||||
*buf = append(*buf, bytesArray...)
|
||||
return nil
|
||||
}
|
||||
|
||||
*buf = append(*buf, '[')
|
||||
|
||||
var started bool
|
||||
for i := 0; i < nb; i++ {
|
||||
n := self.nodeAt(i)
|
||||
if !n.Exists() {
|
||||
continue
|
||||
}
|
||||
if started {
|
||||
*buf = append(*buf, ',')
|
||||
}
|
||||
started = true
|
||||
if err := n.encode(buf); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
*buf = append(*buf, ']')
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *Pair) encode(buf *[]byte) error {
|
||||
if len(*buf) == 0 {
|
||||
*buf = append(*buf, '"', '"', ':')
|
||||
return self.Value.encode(buf)
|
||||
}
|
||||
|
||||
quote(buf, self.Key)
|
||||
*buf = append(*buf, ':')
|
||||
|
||||
return self.Value.encode(buf)
|
||||
}
|
||||
|
||||
func (self *Node) encodeObject(buf *[]byte) error {
|
||||
if self.isLazy() {
|
||||
if err := self.skipAllKey(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
nb := self.len()
|
||||
if nb == 0 {
|
||||
*buf = append(*buf, bytesObject...)
|
||||
return nil
|
||||
}
|
||||
|
||||
*buf = append(*buf, '{')
|
||||
|
||||
var started bool
|
||||
for i := 0; i < nb; i++ {
|
||||
n := self.pairAt(i)
|
||||
if n == nil || !n.Value.Exists() {
|
||||
continue
|
||||
}
|
||||
if started {
|
||||
*buf = append(*buf, ',')
|
||||
}
|
||||
started = true
|
||||
if err := n.encode(buf); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
*buf = append(*buf, '}')
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package ast
|
||||
|
||||
import (
|
||||
`fmt`
|
||||
`strings`
|
||||
`unsafe`
|
||||
|
||||
`github.com/bytedance/sonic/internal/native/types`
|
||||
)
|
||||
|
||||
|
||||
func newError(err types.ParsingError, msg string) *Node {
|
||||
return &Node{
|
||||
t: V_ERROR,
|
||||
l: uint(err),
|
||||
p: unsafe.Pointer(&msg),
|
||||
}
|
||||
}
|
||||
|
||||
func newErrorPair(err SyntaxError) *Pair {
|
||||
return &Pair{0, "", *newSyntaxError(err)}
|
||||
}
|
||||
|
||||
// Error returns error message if the node is invalid
|
||||
func (self Node) Error() string {
|
||||
if self.t != V_ERROR {
|
||||
return ""
|
||||
} else {
|
||||
return *(*string)(self.p)
|
||||
}
|
||||
}
|
||||
|
||||
func newSyntaxError(err SyntaxError) *Node {
|
||||
msg := err.Description()
|
||||
return &Node{
|
||||
t: V_ERROR,
|
||||
l: uint(err.Code),
|
||||
p: unsafe.Pointer(&msg),
|
||||
}
|
||||
}
|
||||
|
||||
func (self *Parser) syntaxError(err types.ParsingError) SyntaxError {
|
||||
return SyntaxError{
|
||||
Pos : self.p,
|
||||
Src : self.s,
|
||||
Code: err,
|
||||
}
|
||||
}
|
||||
|
||||
func unwrapError(err error) *Node {
|
||||
if se, ok := err.(*Node); ok {
|
||||
return se
|
||||
}else if sse, ok := err.(Node); ok {
|
||||
return &sse
|
||||
} else {
|
||||
msg := err.Error()
|
||||
return &Node{
|
||||
t: V_ERROR,
|
||||
p: unsafe.Pointer(&msg),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type SyntaxError struct {
|
||||
Pos int
|
||||
Src string
|
||||
Code types.ParsingError
|
||||
Msg string
|
||||
}
|
||||
|
||||
func (self SyntaxError) Error() string {
|
||||
return fmt.Sprintf("%q", self.Description())
|
||||
}
|
||||
|
||||
func (self SyntaxError) Description() string {
|
||||
return "Syntax error " + self.description()
|
||||
}
|
||||
|
||||
func (self SyntaxError) description() string {
|
||||
i := 16
|
||||
p := self.Pos - i
|
||||
q := self.Pos + i
|
||||
|
||||
/* check for empty source */
|
||||
if self.Src == "" {
|
||||
return fmt.Sprintf("no sources available, the input json is empty: %#v", self)
|
||||
}
|
||||
|
||||
/* prevent slicing before the beginning */
|
||||
if p < 0 {
|
||||
p, q, i = 0, q - p, i + p
|
||||
}
|
||||
|
||||
/* prevent slicing beyond the end */
|
||||
if n := len(self.Src); q > n {
|
||||
n = q - n
|
||||
q = len(self.Src)
|
||||
|
||||
/* move the left bound if possible */
|
||||
if p > n {
|
||||
i += n
|
||||
p -= n
|
||||
}
|
||||
}
|
||||
|
||||
/* left and right length */
|
||||
x := clamp_zero(i)
|
||||
y := clamp_zero(q - p - i - 1)
|
||||
|
||||
/* compose the error description */
|
||||
return fmt.Sprintf(
|
||||
"at index %d: %s\n\n\t%s\n\t%s^%s\n",
|
||||
self.Pos,
|
||||
self.Message(),
|
||||
self.Src[p:q],
|
||||
strings.Repeat(".", x),
|
||||
strings.Repeat(".", y),
|
||||
)
|
||||
}
|
||||
|
||||
func (self SyntaxError) Message() string {
|
||||
if self.Msg == "" {
|
||||
return self.Code.Message()
|
||||
}
|
||||
return self.Msg
|
||||
}
|
||||
|
||||
func clamp_zero(v int) int {
|
||||
if v < 0 {
|
||||
return 0
|
||||
} else {
|
||||
return v
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
/*
|
||||
* Copyright 2021 ByteDance Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ast
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/bytedance/sonic/internal/caching"
|
||||
"github.com/bytedance/sonic/internal/native/types"
|
||||
)
|
||||
|
||||
type Pair struct {
|
||||
hash uint64
|
||||
Key string
|
||||
Value Node
|
||||
}
|
||||
|
||||
func NewPair(key string, val Node) Pair {
|
||||
return Pair{
|
||||
hash: caching.StrHash(key),
|
||||
Key: key,
|
||||
Value: val,
|
||||
}
|
||||
}
|
||||
|
||||
// Values returns iterator for array's children traversal
|
||||
func (self *Node) Values() (ListIterator, error) {
|
||||
if err := self.should(types.V_ARRAY); err != nil {
|
||||
return ListIterator{}, err
|
||||
}
|
||||
return self.values(), nil
|
||||
}
|
||||
|
||||
func (self *Node) values() ListIterator {
|
||||
return ListIterator{Iterator{p: self}}
|
||||
}
|
||||
|
||||
// Properties returns iterator for object's children traversal
|
||||
func (self *Node) Properties() (ObjectIterator, error) {
|
||||
if err := self.should(types.V_OBJECT); err != nil {
|
||||
return ObjectIterator{}, err
|
||||
}
|
||||
return self.properties(), nil
|
||||
}
|
||||
|
||||
func (self *Node) properties() ObjectIterator {
|
||||
return ObjectIterator{Iterator{p: self}}
|
||||
}
|
||||
|
||||
type Iterator struct {
|
||||
i int
|
||||
p *Node
|
||||
}
|
||||
|
||||
func (self *Iterator) Pos() int {
|
||||
return self.i
|
||||
}
|
||||
|
||||
func (self *Iterator) Len() int {
|
||||
return self.p.len()
|
||||
}
|
||||
|
||||
// HasNext reports if it is the end of iteration or has error.
|
||||
func (self *Iterator) HasNext() bool {
|
||||
if !self.p.isLazy() {
|
||||
return self.p.Valid() && self.i < self.p.len()
|
||||
} else if self.p.t == _V_ARRAY_LAZY {
|
||||
return self.p.skipNextNode().Valid()
|
||||
} else if self.p.t == _V_OBJECT_LAZY {
|
||||
pair := self.p.skipNextPair()
|
||||
if pair == nil {
|
||||
return false
|
||||
}
|
||||
return pair.Value.Valid()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ListIterator is specialized iterator for V_ARRAY
|
||||
type ListIterator struct {
|
||||
Iterator
|
||||
}
|
||||
|
||||
// ObjectIterator is specialized iterator for V_ARRAY
|
||||
type ObjectIterator struct {
|
||||
Iterator
|
||||
}
|
||||
|
||||
func (self *ListIterator) next() *Node {
|
||||
next_start:
|
||||
if !self.HasNext() {
|
||||
return nil
|
||||
} else {
|
||||
n := self.p.nodeAt(self.i)
|
||||
self.i++
|
||||
if !n.Exists() {
|
||||
goto next_start
|
||||
}
|
||||
return n
|
||||
}
|
||||
}
|
||||
|
||||
// Next scans through children of underlying V_ARRAY,
|
||||
// copies each child to v, and returns .HasNext().
|
||||
func (self *ListIterator) Next(v *Node) bool {
|
||||
n := self.next()
|
||||
if n == nil {
|
||||
return false
|
||||
}
|
||||
*v = *n
|
||||
return true
|
||||
}
|
||||
|
||||
func (self *ObjectIterator) next() *Pair {
|
||||
next_start:
|
||||
if !self.HasNext() {
|
||||
return nil
|
||||
} else {
|
||||
n := self.p.pairAt(self.i)
|
||||
self.i++
|
||||
if n == nil || !n.Value.Exists() {
|
||||
goto next_start
|
||||
}
|
||||
return n
|
||||
}
|
||||
}
|
||||
|
||||
// Next scans through children of underlying V_OBJECT,
|
||||
// copies each child to v, and returns .HasNext().
|
||||
func (self *ObjectIterator) Next(p *Pair) bool {
|
||||
n := self.next()
|
||||
if n == nil {
|
||||
return false
|
||||
}
|
||||
*p = *n
|
||||
return true
|
||||
}
|
||||
|
||||
// Sequence represents scanning path of single-layer nodes.
|
||||
// Index indicates the value's order in both V_ARRAY and V_OBJECT json.
|
||||
// Key is the value's key (for V_OBJECT json only, otherwise it will be nil).
|
||||
type Sequence struct {
|
||||
Index int
|
||||
Key *string
|
||||
// Level int
|
||||
}
|
||||
|
||||
// String is string representation of one Sequence
|
||||
func (s Sequence) String() string {
|
||||
k := ""
|
||||
if s.Key != nil {
|
||||
k = *s.Key
|
||||
}
|
||||
return fmt.Sprintf("Sequence(%d, %q)", s.Index, k)
|
||||
}
|
||||
|
||||
type Scanner func(path Sequence, node *Node) bool
|
||||
|
||||
// ForEach scans one V_OBJECT node's children from JSON head to tail,
|
||||
// and pass the Sequence and Node of corresponding JSON value.
|
||||
//
|
||||
// Especially, if the node is not V_ARRAY or V_OBJECT,
|
||||
// the node itself will be returned and Sequence.Index == -1.
|
||||
//
|
||||
// NOTICE: An unset node WON'T trigger sc, but its index still counts into Path.Index
|
||||
func (self *Node) ForEach(sc Scanner) error {
|
||||
if err := self.checkRaw(); err != nil {
|
||||
return err
|
||||
}
|
||||
switch self.itype() {
|
||||
case types.V_ARRAY:
|
||||
iter, err := self.Values()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
v := iter.next()
|
||||
for v != nil {
|
||||
if !sc(Sequence{iter.i-1, nil}, v) {
|
||||
return nil
|
||||
}
|
||||
v = iter.next()
|
||||
}
|
||||
case types.V_OBJECT:
|
||||
iter, err := self.Properties()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
v := iter.next()
|
||||
for v != nil {
|
||||
if !sc(Sequence{iter.i-1, &v.Key}, &v.Value) {
|
||||
return nil
|
||||
}
|
||||
v = iter.next()
|
||||
}
|
||||
default:
|
||||
if self.Check() != nil {
|
||||
return self
|
||||
}
|
||||
sc(Sequence{-1, nil}, self)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,768 @@
|
||||
/*
|
||||
* Copyright 2021 ByteDance Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ast
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/bytedance/sonic/internal/native/types"
|
||||
"github.com/bytedance/sonic/internal/rt"
|
||||
"github.com/bytedance/sonic/internal/utils"
|
||||
"github.com/bytedance/sonic/unquote"
|
||||
)
|
||||
|
||||
const (
|
||||
_DEFAULT_NODE_CAP int = 16
|
||||
_APPEND_GROW_SHIFT = 1
|
||||
)
|
||||
|
||||
const (
|
||||
_ERR_NOT_FOUND types.ParsingError = 33
|
||||
_ERR_UNSUPPORT_TYPE types.ParsingError = 34
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrNotExist means both key and value doesn't exist
|
||||
ErrNotExist error = newError(_ERR_NOT_FOUND, "value not exists")
|
||||
|
||||
// ErrUnsupportType means API on the node is unsupported
|
||||
ErrUnsupportType error = newError(_ERR_UNSUPPORT_TYPE, "unsupported type")
|
||||
)
|
||||
|
||||
type Parser struct {
|
||||
p int
|
||||
s string
|
||||
noLazy bool
|
||||
loadOnce bool
|
||||
skipValue bool
|
||||
dbuf *byte
|
||||
}
|
||||
|
||||
/** Parser Private Methods **/
|
||||
|
||||
func (self *Parser) delim() types.ParsingError {
|
||||
n := len(self.s)
|
||||
p := self.lspace(self.p)
|
||||
|
||||
/* check for EOF */
|
||||
if p >= n {
|
||||
return types.ERR_EOF
|
||||
}
|
||||
|
||||
/* check for the delimiter */
|
||||
if self.s[p] != ':' {
|
||||
return types.ERR_INVALID_CHAR
|
||||
}
|
||||
|
||||
/* update the read pointer */
|
||||
self.p = p + 1
|
||||
return 0
|
||||
}
|
||||
|
||||
func (self *Parser) object() types.ParsingError {
|
||||
n := len(self.s)
|
||||
p := self.lspace(self.p)
|
||||
|
||||
/* check for EOF */
|
||||
if p >= n {
|
||||
return types.ERR_EOF
|
||||
}
|
||||
|
||||
/* check for the delimiter */
|
||||
if self.s[p] != '{' {
|
||||
return types.ERR_INVALID_CHAR
|
||||
}
|
||||
|
||||
/* update the read pointer */
|
||||
self.p = p + 1
|
||||
return 0
|
||||
}
|
||||
|
||||
func (self *Parser) array() types.ParsingError {
|
||||
n := len(self.s)
|
||||
p := self.lspace(self.p)
|
||||
|
||||
/* check for EOF */
|
||||
if p >= n {
|
||||
return types.ERR_EOF
|
||||
}
|
||||
|
||||
/* check for the delimiter */
|
||||
if self.s[p] != '[' {
|
||||
return types.ERR_INVALID_CHAR
|
||||
}
|
||||
|
||||
/* update the read pointer */
|
||||
self.p = p + 1
|
||||
return 0
|
||||
}
|
||||
|
||||
func (self *Parser) lspace(sp int) int {
|
||||
ns := len(self.s)
|
||||
for ; sp<ns && utils.IsSpace(self.s[sp]); sp+=1 {}
|
||||
|
||||
return sp
|
||||
}
|
||||
|
||||
func (self *Parser) backward() {
|
||||
for ; self.p >= 0 && utils.IsSpace(self.s[self.p]); self.p-=1 {}
|
||||
}
|
||||
|
||||
func (self *Parser) decodeArray(ret *linkedNodes) (Node, types.ParsingError) {
|
||||
sp := self.p
|
||||
ns := len(self.s)
|
||||
|
||||
/* check for EOF */
|
||||
if self.p = self.lspace(sp); self.p >= ns {
|
||||
return Node{}, types.ERR_EOF
|
||||
}
|
||||
|
||||
/* check for empty array */
|
||||
if self.s[self.p] == ']' {
|
||||
self.p++
|
||||
return Node{t: types.V_ARRAY}, 0
|
||||
}
|
||||
|
||||
/* allocate array space and parse every element */
|
||||
for {
|
||||
var val Node
|
||||
var err types.ParsingError
|
||||
|
||||
if self.skipValue {
|
||||
/* skip the value */
|
||||
var start int
|
||||
if start, err = self.skipFast(); err != 0 {
|
||||
return Node{}, err
|
||||
}
|
||||
if self.p > ns {
|
||||
return Node{}, types.ERR_EOF
|
||||
}
|
||||
t := switchRawType(self.s[start])
|
||||
if t == _V_NONE {
|
||||
return Node{}, types.ERR_INVALID_CHAR
|
||||
}
|
||||
val = newRawNode(self.s[start:self.p], t, false)
|
||||
}else{
|
||||
/* decode the value */
|
||||
if val, err = self.Parse(); err != 0 {
|
||||
return Node{}, err
|
||||
}
|
||||
}
|
||||
|
||||
/* add the value to result */
|
||||
ret.Push(val)
|
||||
self.p = self.lspace(self.p)
|
||||
|
||||
/* check for EOF */
|
||||
if self.p >= ns {
|
||||
return Node{}, types.ERR_EOF
|
||||
}
|
||||
|
||||
/* check for the next character */
|
||||
switch self.s[self.p] {
|
||||
case ',' : self.p++
|
||||
case ']' : self.p++; return newArray(ret), 0
|
||||
default:
|
||||
// if val.isLazy() {
|
||||
// return newLazyArray(self, ret), 0
|
||||
// }
|
||||
return Node{}, types.ERR_INVALID_CHAR
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (self *Parser) decodeObject(ret *linkedPairs) (Node, types.ParsingError) {
|
||||
sp := self.p
|
||||
ns := len(self.s)
|
||||
|
||||
/* check for EOF */
|
||||
if self.p = self.lspace(sp); self.p >= ns {
|
||||
return Node{}, types.ERR_EOF
|
||||
}
|
||||
|
||||
/* check for empty object */
|
||||
if self.s[self.p] == '}' {
|
||||
self.p++
|
||||
return Node{t: types.V_OBJECT}, 0
|
||||
}
|
||||
|
||||
/* decode each pair */
|
||||
for {
|
||||
var val Node
|
||||
var njs types.JsonState
|
||||
var err types.ParsingError
|
||||
|
||||
/* decode the key */
|
||||
if njs = self.decodeValue(); njs.Vt != types.V_STRING {
|
||||
return Node{}, types.ERR_INVALID_CHAR
|
||||
}
|
||||
|
||||
/* extract the key */
|
||||
idx := self.p - 1
|
||||
key := self.s[njs.Iv:idx]
|
||||
|
||||
/* check for escape sequence */
|
||||
if njs.Ep != -1 {
|
||||
if key, err = unquote.String(key); err != 0 {
|
||||
return Node{}, err
|
||||
}
|
||||
}
|
||||
|
||||
/* expect a ':' delimiter */
|
||||
if err = self.delim(); err != 0 {
|
||||
return Node{}, err
|
||||
}
|
||||
|
||||
|
||||
if self.skipValue {
|
||||
/* skip the value */
|
||||
var start int
|
||||
if start, err = self.skipFast(); err != 0 {
|
||||
return Node{}, err
|
||||
}
|
||||
if self.p > ns {
|
||||
return Node{}, types.ERR_EOF
|
||||
}
|
||||
t := switchRawType(self.s[start])
|
||||
if t == _V_NONE {
|
||||
return Node{}, types.ERR_INVALID_CHAR
|
||||
}
|
||||
val = newRawNode(self.s[start:self.p], t, false)
|
||||
} else {
|
||||
/* decode the value */
|
||||
if val, err = self.Parse(); err != 0 {
|
||||
return Node{}, err
|
||||
}
|
||||
}
|
||||
|
||||
/* add the value to result */
|
||||
// FIXME: ret's address may change here, thus previous referred node in ret may be invalid !!
|
||||
ret.Push(NewPair(key, val))
|
||||
self.p = self.lspace(self.p)
|
||||
|
||||
/* check for EOF */
|
||||
if self.p >= ns {
|
||||
return Node{}, types.ERR_EOF
|
||||
}
|
||||
|
||||
/* check for the next character */
|
||||
switch self.s[self.p] {
|
||||
case ',' : self.p++
|
||||
case '}' : self.p++; return newObject(ret), 0
|
||||
default:
|
||||
// if val.isLazy() {
|
||||
// return newLazyObject(self, ret), 0
|
||||
// }
|
||||
return Node{}, types.ERR_INVALID_CHAR
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (self *Parser) decodeString(iv int64, ep int) (Node, types.ParsingError) {
|
||||
p := self.p - 1
|
||||
s := self.s[iv:p]
|
||||
|
||||
/* fast path: no escape sequence */
|
||||
if ep == -1 {
|
||||
return NewString(s), 0
|
||||
}
|
||||
|
||||
/* unquote the string */
|
||||
out, err := unquote.String(s)
|
||||
|
||||
/* check for errors */
|
||||
if err != 0 {
|
||||
return Node{}, err
|
||||
} else {
|
||||
return newBytes(rt.Str2Mem(out)), 0
|
||||
}
|
||||
}
|
||||
|
||||
/** Parser Interface **/
|
||||
|
||||
func (self *Parser) Pos() int {
|
||||
return self.p
|
||||
}
|
||||
|
||||
|
||||
// Parse returns a ast.Node representing the parser's JSON.
|
||||
// NOTICE: the specific parsing lazy dependens parser's option
|
||||
// It only parse first layer and first child for Object or Array be default
|
||||
func (self *Parser) Parse() (Node, types.ParsingError) {
|
||||
switch val := self.decodeValue(); val.Vt {
|
||||
case types.V_EOF : return Node{}, types.ERR_EOF
|
||||
case types.V_NULL : return nullNode, 0
|
||||
case types.V_TRUE : return trueNode, 0
|
||||
case types.V_FALSE : return falseNode, 0
|
||||
case types.V_STRING : return self.decodeString(val.Iv, val.Ep)
|
||||
case types.V_ARRAY:
|
||||
s := self.p - 1;
|
||||
if p := skipBlank(self.s, self.p); p >= self.p && self.s[p] == ']' {
|
||||
self.p = p + 1
|
||||
return Node{t: types.V_ARRAY}, 0
|
||||
}
|
||||
if self.noLazy {
|
||||
if self.loadOnce {
|
||||
self.noLazy = false
|
||||
}
|
||||
return self.decodeArray(new(linkedNodes))
|
||||
}
|
||||
// NOTICE: loadOnce always keep raw json for object or array
|
||||
if self.loadOnce {
|
||||
self.p = s
|
||||
s, e := self.skipFast()
|
||||
if e != 0 {
|
||||
return Node{}, e
|
||||
}
|
||||
return newRawNode(self.s[s:self.p], types.V_ARRAY, true), 0
|
||||
}
|
||||
return newLazyArray(self), 0
|
||||
case types.V_OBJECT:
|
||||
s := self.p - 1;
|
||||
if p := skipBlank(self.s, self.p); p >= self.p && self.s[p] == '}' {
|
||||
self.p = p + 1
|
||||
return Node{t: types.V_OBJECT}, 0
|
||||
}
|
||||
// NOTICE: loadOnce always keep raw json for object or array
|
||||
if self.noLazy {
|
||||
if self.loadOnce {
|
||||
self.noLazy = false
|
||||
}
|
||||
return self.decodeObject(new(linkedPairs))
|
||||
}
|
||||
if self.loadOnce {
|
||||
self.p = s
|
||||
s, e := self.skipFast()
|
||||
if e != 0 {
|
||||
return Node{}, e
|
||||
}
|
||||
return newRawNode(self.s[s:self.p], types.V_OBJECT, true), 0
|
||||
}
|
||||
return newLazyObject(self), 0
|
||||
case types.V_DOUBLE : return NewNumber(self.s[val.Ep:self.p]), 0
|
||||
case types.V_INTEGER : return NewNumber(self.s[val.Ep:self.p]), 0
|
||||
default : return Node{}, types.ParsingError(-val.Vt)
|
||||
}
|
||||
}
|
||||
|
||||
func (self *Parser) searchKey(match string) types.ParsingError {
|
||||
ns := len(self.s)
|
||||
if err := self.object(); err != 0 {
|
||||
return err
|
||||
}
|
||||
|
||||
/* check for EOF */
|
||||
if self.p = self.lspace(self.p); self.p >= ns {
|
||||
return types.ERR_EOF
|
||||
}
|
||||
|
||||
/* check for empty object */
|
||||
if self.s[self.p] == '}' {
|
||||
self.p++
|
||||
return _ERR_NOT_FOUND
|
||||
}
|
||||
|
||||
var njs types.JsonState
|
||||
var err types.ParsingError
|
||||
/* decode each pair */
|
||||
for {
|
||||
|
||||
/* decode the key */
|
||||
if njs = self.decodeValue(); njs.Vt != types.V_STRING {
|
||||
return types.ERR_INVALID_CHAR
|
||||
}
|
||||
|
||||
/* extract the key */
|
||||
idx := self.p - 1
|
||||
key := self.s[njs.Iv:idx]
|
||||
|
||||
/* check for escape sequence */
|
||||
if njs.Ep != -1 {
|
||||
if key, err = unquote.String(key); err != 0 {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
/* expect a ':' delimiter */
|
||||
if err = self.delim(); err != 0 {
|
||||
return err
|
||||
}
|
||||
|
||||
/* skip value */
|
||||
if key != match {
|
||||
if _, err = self.skipFast(); err != 0 {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
|
||||
/* check for EOF */
|
||||
self.p = self.lspace(self.p)
|
||||
if self.p >= ns {
|
||||
return types.ERR_EOF
|
||||
}
|
||||
|
||||
/* check for the next character */
|
||||
switch self.s[self.p] {
|
||||
case ',':
|
||||
self.p++
|
||||
case '}':
|
||||
self.p++
|
||||
return _ERR_NOT_FOUND
|
||||
default:
|
||||
return types.ERR_INVALID_CHAR
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (self *Parser) searchIndex(idx int) types.ParsingError {
|
||||
ns := len(self.s)
|
||||
if err := self.array(); err != 0 {
|
||||
return err
|
||||
}
|
||||
|
||||
/* check for EOF */
|
||||
if self.p = self.lspace(self.p); self.p >= ns {
|
||||
return types.ERR_EOF
|
||||
}
|
||||
|
||||
/* check for empty array */
|
||||
if self.s[self.p] == ']' {
|
||||
self.p++
|
||||
return _ERR_NOT_FOUND
|
||||
}
|
||||
|
||||
var err types.ParsingError
|
||||
/* allocate array space and parse every element */
|
||||
for i := 0; i < idx; i++ {
|
||||
|
||||
/* decode the value */
|
||||
if _, err = self.skipFast(); err != 0 {
|
||||
return err
|
||||
}
|
||||
|
||||
/* check for EOF */
|
||||
self.p = self.lspace(self.p)
|
||||
if self.p >= ns {
|
||||
return types.ERR_EOF
|
||||
}
|
||||
|
||||
/* check for the next character */
|
||||
switch self.s[self.p] {
|
||||
case ',':
|
||||
self.p++
|
||||
case ']':
|
||||
self.p++
|
||||
return _ERR_NOT_FOUND
|
||||
default:
|
||||
return types.ERR_INVALID_CHAR
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
func (self *Node) skipNextNode() *Node {
|
||||
if !self.isLazy() {
|
||||
return nil
|
||||
}
|
||||
|
||||
parser, stack := self.getParserAndArrayStack()
|
||||
ret := &stack.v
|
||||
sp := parser.p
|
||||
ns := len(parser.s)
|
||||
|
||||
/* check for EOF */
|
||||
if parser.p = parser.lspace(sp); parser.p >= ns {
|
||||
return newSyntaxError(parser.syntaxError(types.ERR_EOF))
|
||||
}
|
||||
|
||||
/* check for empty array */
|
||||
if parser.s[parser.p] == ']' {
|
||||
parser.p++
|
||||
self.setArray(ret)
|
||||
return nil
|
||||
}
|
||||
|
||||
var val Node
|
||||
/* skip the value */
|
||||
if start, err := parser.skipFast(); err != 0 {
|
||||
return newSyntaxError(parser.syntaxError(err))
|
||||
} else {
|
||||
t := switchRawType(parser.s[start])
|
||||
if t == _V_NONE {
|
||||
return newSyntaxError(parser.syntaxError(types.ERR_INVALID_CHAR))
|
||||
}
|
||||
val = newRawNode(parser.s[start:parser.p], t, false)
|
||||
}
|
||||
|
||||
/* add the value to result */
|
||||
ret.Push(val)
|
||||
self.l++
|
||||
parser.p = parser.lspace(parser.p)
|
||||
|
||||
/* check for EOF */
|
||||
if parser.p >= ns {
|
||||
return newSyntaxError(parser.syntaxError(types.ERR_EOF))
|
||||
}
|
||||
|
||||
/* check for the next character */
|
||||
switch parser.s[parser.p] {
|
||||
case ',':
|
||||
parser.p++
|
||||
return ret.At(ret.Len()-1)
|
||||
case ']':
|
||||
parser.p++
|
||||
self.setArray(ret)
|
||||
return ret.At(ret.Len()-1)
|
||||
default:
|
||||
return newSyntaxError(parser.syntaxError(types.ERR_INVALID_CHAR))
|
||||
}
|
||||
}
|
||||
|
||||
func (self *Node) skipNextPair() (*Pair) {
|
||||
if !self.isLazy() {
|
||||
return nil
|
||||
}
|
||||
|
||||
parser, stack := self.getParserAndObjectStack()
|
||||
ret := &stack.v
|
||||
sp := parser.p
|
||||
ns := len(parser.s)
|
||||
|
||||
/* check for EOF */
|
||||
if parser.p = parser.lspace(sp); parser.p >= ns {
|
||||
return newErrorPair(parser.syntaxError(types.ERR_EOF))
|
||||
}
|
||||
|
||||
/* check for empty object */
|
||||
if parser.s[parser.p] == '}' {
|
||||
parser.p++
|
||||
self.setObject(ret)
|
||||
return nil
|
||||
}
|
||||
|
||||
/* decode one pair */
|
||||
var val Node
|
||||
var njs types.JsonState
|
||||
var err types.ParsingError
|
||||
|
||||
/* decode the key */
|
||||
if njs = parser.decodeValue(); njs.Vt != types.V_STRING {
|
||||
return newErrorPair(parser.syntaxError(types.ERR_INVALID_CHAR))
|
||||
}
|
||||
|
||||
/* extract the key */
|
||||
idx := parser.p - 1
|
||||
key := parser.s[njs.Iv:idx]
|
||||
|
||||
/* check for escape sequence */
|
||||
if njs.Ep != -1 {
|
||||
if key, err = unquote.String(key); err != 0 {
|
||||
return newErrorPair(parser.syntaxError(err))
|
||||
}
|
||||
}
|
||||
|
||||
/* expect a ':' delimiter */
|
||||
if err = parser.delim(); err != 0 {
|
||||
return newErrorPair(parser.syntaxError(err))
|
||||
}
|
||||
|
||||
/* skip the value */
|
||||
if start, err := parser.skipFast(); err != 0 {
|
||||
return newErrorPair(parser.syntaxError(err))
|
||||
} else {
|
||||
t := switchRawType(parser.s[start])
|
||||
if t == _V_NONE {
|
||||
return newErrorPair(parser.syntaxError(types.ERR_INVALID_CHAR))
|
||||
}
|
||||
val = newRawNode(parser.s[start:parser.p], t, false)
|
||||
}
|
||||
|
||||
/* add the value to result */
|
||||
ret.Push(NewPair(key, val))
|
||||
self.l++
|
||||
parser.p = parser.lspace(parser.p)
|
||||
|
||||
/* check for EOF */
|
||||
if parser.p >= ns {
|
||||
return newErrorPair(parser.syntaxError(types.ERR_EOF))
|
||||
}
|
||||
|
||||
/* check for the next character */
|
||||
switch parser.s[parser.p] {
|
||||
case ',':
|
||||
parser.p++
|
||||
return ret.At(ret.Len()-1)
|
||||
case '}':
|
||||
parser.p++
|
||||
self.setObject(ret)
|
||||
return ret.At(ret.Len()-1)
|
||||
default:
|
||||
return newErrorPair(parser.syntaxError(types.ERR_INVALID_CHAR))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** Parser Factory **/
|
||||
|
||||
// Loads parse all json into interface{}
|
||||
func Loads(src string) (int, interface{}, error) {
|
||||
ps := &Parser{s: src}
|
||||
np, err := ps.Parse()
|
||||
|
||||
/* check for errors */
|
||||
if err != 0 {
|
||||
return 0, nil, ps.ExportError(err)
|
||||
} else {
|
||||
x, err := np.Interface()
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
return ps.Pos(), x, nil
|
||||
}
|
||||
}
|
||||
|
||||
// LoadsUseNumber parse all json into interface{}, with numeric nodes cast to json.Number
|
||||
func LoadsUseNumber(src string) (int, interface{}, error) {
|
||||
ps := &Parser{s: src}
|
||||
np, err := ps.Parse()
|
||||
|
||||
/* check for errors */
|
||||
if err != 0 {
|
||||
return 0, nil, err
|
||||
} else {
|
||||
x, err := np.InterfaceUseNumber()
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
return ps.Pos(), x, nil
|
||||
}
|
||||
}
|
||||
|
||||
// NewParser returns pointer of new allocated parser
|
||||
func NewParser(src string) *Parser {
|
||||
return &Parser{s: src}
|
||||
}
|
||||
|
||||
// NewParser returns new allocated parser
|
||||
func NewParserObj(src string) Parser {
|
||||
return Parser{s: src}
|
||||
}
|
||||
|
||||
// decodeNumber controls if parser decodes the number values instead of skip them
|
||||
// WARN: once you set decodeNumber(true), please set decodeNumber(false) before you drop the parser
|
||||
// otherwise the memory CANNOT be reused
|
||||
func (self *Parser) decodeNumber(decode bool) {
|
||||
if !decode && self.dbuf != nil {
|
||||
types.FreeDbuf(self.dbuf)
|
||||
self.dbuf = nil
|
||||
return
|
||||
}
|
||||
if decode && self.dbuf == nil {
|
||||
self.dbuf = types.NewDbuf()
|
||||
}
|
||||
}
|
||||
|
||||
// ExportError converts types.ParsingError to std Error
|
||||
func (self *Parser) ExportError(err types.ParsingError) error {
|
||||
if err == _ERR_NOT_FOUND {
|
||||
return ErrNotExist
|
||||
}
|
||||
return fmt.Errorf("%q", SyntaxError{
|
||||
Pos : self.p,
|
||||
Src : self.s,
|
||||
Code: err,
|
||||
}.Description())
|
||||
}
|
||||
|
||||
func backward(src string, i int) int {
|
||||
for ; i>=0 && utils.IsSpace(src[i]); i-- {}
|
||||
return i
|
||||
}
|
||||
|
||||
|
||||
func newRawNode(str string, typ types.ValueType, lock bool) Node {
|
||||
ret := Node{
|
||||
t: typ | _V_RAW,
|
||||
p: rt.StrPtr(str),
|
||||
l: uint(len(str)),
|
||||
}
|
||||
if lock {
|
||||
ret.m = new(sync.RWMutex)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
var typeJumpTable = [256]types.ValueType{
|
||||
'"' : types.V_STRING,
|
||||
'-' : _V_NUMBER,
|
||||
'0' : _V_NUMBER,
|
||||
'1' : _V_NUMBER,
|
||||
'2' : _V_NUMBER,
|
||||
'3' : _V_NUMBER,
|
||||
'4' : _V_NUMBER,
|
||||
'5' : _V_NUMBER,
|
||||
'6' : _V_NUMBER,
|
||||
'7' : _V_NUMBER,
|
||||
'8' : _V_NUMBER,
|
||||
'9' : _V_NUMBER,
|
||||
'[' : types.V_ARRAY,
|
||||
'f' : types.V_FALSE,
|
||||
'n' : types.V_NULL,
|
||||
't' : types.V_TRUE,
|
||||
'{' : types.V_OBJECT,
|
||||
}
|
||||
|
||||
func switchRawType(c byte) types.ValueType {
|
||||
return typeJumpTable[c]
|
||||
}
|
||||
|
||||
func (self *Node) loadt() types.ValueType {
|
||||
return (types.ValueType)(atomic.LoadInt64(&self.t))
|
||||
}
|
||||
|
||||
func (self *Node) lock() bool {
|
||||
if m := self.m; m != nil {
|
||||
m.Lock()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (self *Node) unlock() {
|
||||
if m := self.m; m != nil {
|
||||
m.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func (self *Node) rlock() bool {
|
||||
if m := self.m; m != nil {
|
||||
m.RLock()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (self *Node) runlock() {
|
||||
if m := self.m; m != nil {
|
||||
m.RUnlock()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
/*
|
||||
* Copyright 2021 ByteDance Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ast
|
||||
|
||||
import (
|
||||
`github.com/bytedance/sonic/internal/rt`
|
||||
`github.com/bytedance/sonic/internal/native/types`
|
||||
)
|
||||
|
||||
// SearchOptions controls Searcher's behavior
|
||||
type SearchOptions struct {
|
||||
// ValidateJSON indicates the searcher to validate the entire JSON
|
||||
ValidateJSON bool
|
||||
|
||||
// CopyReturn indicates the searcher to copy the result JSON instead of refer from the input
|
||||
// This can help to reduce memory usage if you cache the results
|
||||
CopyReturn bool
|
||||
|
||||
// ConcurrentRead indicates the searcher to return a concurrently-READ-safe node,
|
||||
// including: GetByPath/Get/Index/GetOrIndex/Int64/Bool/Float64/String/Number/Interface/Array/Map/Raw/MarshalJSON
|
||||
ConcurrentRead bool
|
||||
}
|
||||
|
||||
type Searcher struct {
|
||||
parser Parser
|
||||
SearchOptions
|
||||
}
|
||||
|
||||
func NewSearcher(str string) *Searcher {
|
||||
return &Searcher{
|
||||
parser: Parser{
|
||||
s: str,
|
||||
noLazy: false,
|
||||
},
|
||||
SearchOptions: SearchOptions{
|
||||
ValidateJSON: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// GetByPathCopy search in depth from top json and returns a **Copied** json node at the path location
|
||||
func (self *Searcher) GetByPathCopy(path ...interface{}) (Node, error) {
|
||||
self.CopyReturn = true
|
||||
return self.getByPath(path...)
|
||||
}
|
||||
|
||||
// GetByPathNoCopy search in depth from top json and returns a **Referenced** json node at the path location
|
||||
//
|
||||
// WARN: this search directly refer partial json from top json, which has faster speed,
|
||||
// may consumes more memory.
|
||||
func (self *Searcher) GetByPath(path ...interface{}) (Node, error) {
|
||||
return self.getByPath(path...)
|
||||
}
|
||||
|
||||
func (self *Searcher) getByPath(path ...interface{}) (Node, error) {
|
||||
var err types.ParsingError
|
||||
var start int
|
||||
|
||||
self.parser.p = 0
|
||||
start, err = self.parser.getByPath(self.ValidateJSON, path...)
|
||||
if err != 0 {
|
||||
// for compatibility with old version
|
||||
if err == types.ERR_NOT_FOUND {
|
||||
return Node{}, ErrNotExist
|
||||
}
|
||||
if err == types.ERR_UNSUPPORT_TYPE {
|
||||
panic("path must be either int(>=0) or string")
|
||||
}
|
||||
return Node{}, self.parser.syntaxError(err)
|
||||
}
|
||||
|
||||
t := switchRawType(self.parser.s[start])
|
||||
if t == _V_NONE {
|
||||
return Node{}, self.parser.ExportError(err)
|
||||
}
|
||||
|
||||
// copy string to reducing memory usage
|
||||
var raw string
|
||||
if self.CopyReturn {
|
||||
raw = rt.Mem2Str([]byte(self.parser.s[start:self.parser.p]))
|
||||
} else {
|
||||
raw = self.parser.s[start:self.parser.p]
|
||||
}
|
||||
return newRawNode(raw, t, self.ConcurrentRead), nil
|
||||
}
|
||||
|
||||
// GetByPath searches a path and returns relaction and types of target
|
||||
func _GetByPath(src string, path ...interface{}) (start int, end int, typ int, err error) {
|
||||
p := NewParserObj(src)
|
||||
s, e := p.getByPath(false, path...)
|
||||
if e != 0 {
|
||||
// for compatibility with old version
|
||||
if e == types.ERR_NOT_FOUND {
|
||||
return -1, -1, 0, ErrNotExist
|
||||
}
|
||||
if e == types.ERR_UNSUPPORT_TYPE {
|
||||
panic("path must be either int(>=0) or string")
|
||||
}
|
||||
return -1, -1, 0, p.syntaxError(e)
|
||||
}
|
||||
|
||||
t := switchRawType(p.s[s])
|
||||
if t == _V_NONE {
|
||||
return -1, -1, 0, ErrNotExist
|
||||
}
|
||||
if t == _V_NUMBER {
|
||||
p.p = 1 + backward(p.s, p.p-1)
|
||||
}
|
||||
return s, p.p, int(t), nil
|
||||
}
|
||||
|
||||
// ValidSyntax check if a json has a valid JSON syntax,
|
||||
// while not validate UTF-8 charset
|
||||
func _ValidSyntax(json string) bool {
|
||||
p := NewParserObj(json)
|
||||
_, e := p.skip()
|
||||
if e != 0 {
|
||||
return false
|
||||
}
|
||||
if skipBlank(p.s, p.p) != -int(types.ERR_EOF) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// SkipFast skip a json value in fast-skip algs,
|
||||
// while not strictly validate JSON syntax and UTF-8 charset.
|
||||
func _SkipFast(src string, i int) (int, int, error) {
|
||||
p := NewParserObj(src)
|
||||
p.p = i
|
||||
s, e := p.skipFast()
|
||||
if e != 0 {
|
||||
return -1, -1, p.ExportError(e)
|
||||
}
|
||||
t := switchRawType(p.s[s])
|
||||
if t == _V_NONE {
|
||||
return -1, -1, ErrNotExist
|
||||
}
|
||||
if t == _V_NUMBER {
|
||||
p.p = 1 + backward(p.s, p.p-1)
|
||||
}
|
||||
return s, p.p, nil
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2021 ByteDance Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ast
|
||||
|
||||
import (
|
||||
"unsafe"
|
||||
|
||||
"github.com/bytedance/sonic/internal/rt"
|
||||
)
|
||||
|
||||
//go:nosplit
|
||||
func mem2ptr(s []byte) unsafe.Pointer {
|
||||
return (*rt.GoSlice)(unsafe.Pointer(&s)).Ptr
|
||||
}
|
||||
@@ -0,0 +1,333 @@
|
||||
/*
|
||||
* Copyright 2021 ByteDance Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package ast
|
||||
|
||||
import (
|
||||
`encoding/json`
|
||||
`errors`
|
||||
|
||||
`github.com/bytedance/sonic/internal/native/types`
|
||||
`github.com/bytedance/sonic/unquote`
|
||||
)
|
||||
|
||||
// Visitor handles the callbacks during preorder traversal of a JSON AST.
|
||||
//
|
||||
// According to the JSON RFC8259, a JSON AST can be defined by
|
||||
// the following rules without separator / whitespace tokens.
|
||||
//
|
||||
// JSON-AST = value
|
||||
// value = false / null / true / object / array / number / string
|
||||
// object = begin-object [ member *( member ) ] end-object
|
||||
// member = string value
|
||||
// array = begin-array [ value *( value ) ] end-array
|
||||
//
|
||||
type Visitor interface {
|
||||
|
||||
// OnNull handles a JSON null value.
|
||||
OnNull() error
|
||||
|
||||
// OnBool handles a JSON true / false value.
|
||||
OnBool(v bool) error
|
||||
|
||||
// OnString handles a JSON string value.
|
||||
OnString(v string) error
|
||||
|
||||
// OnInt64 handles a JSON number value with int64 type.
|
||||
OnInt64(v int64, n json.Number) error
|
||||
|
||||
// OnFloat64 handles a JSON number value with float64 type.
|
||||
OnFloat64(v float64, n json.Number) error
|
||||
|
||||
// OnObjectBegin handles the beginning of a JSON object value with a
|
||||
// suggested capacity that can be used to make your custom object container.
|
||||
//
|
||||
// After this point the visitor will receive a sequence of callbacks like
|
||||
// [string, value, string, value, ......, ObjectEnd].
|
||||
//
|
||||
// Note:
|
||||
// 1. This is a recursive definition which means the value can
|
||||
// also be a JSON object / array described by a sequence of callbacks.
|
||||
// 2. The suggested capacity will be 0 if current object is empty.
|
||||
// 3. Currently sonic use a fixed capacity for non-empty object (keep in
|
||||
// sync with ast.Node) which might not be very suitable. This may be
|
||||
// improved in future version.
|
||||
OnObjectBegin(capacity int) error
|
||||
|
||||
// OnObjectKey handles a JSON object key string in member.
|
||||
OnObjectKey(key string) error
|
||||
|
||||
// OnObjectEnd handles the ending of a JSON object value.
|
||||
OnObjectEnd() error
|
||||
|
||||
// OnArrayBegin handles the beginning of a JSON array value with a
|
||||
// suggested capacity that can be used to make your custom array container.
|
||||
//
|
||||
// After this point the visitor will receive a sequence of callbacks like
|
||||
// [value, value, value, ......, ArrayEnd].
|
||||
//
|
||||
// Note:
|
||||
// 1. This is a recursive definition which means the value can
|
||||
// also be a JSON object / array described by a sequence of callbacks.
|
||||
// 2. The suggested capacity will be 0 if current array is empty.
|
||||
// 3. Currently sonic use a fixed capacity for non-empty array (keep in
|
||||
// sync with ast.Node) which might not be very suitable. This may be
|
||||
// improved in future version.
|
||||
OnArrayBegin(capacity int) error
|
||||
|
||||
// OnArrayEnd handles the ending of a JSON array value.
|
||||
OnArrayEnd() error
|
||||
}
|
||||
|
||||
// VisitorOptions contains all Visitor's options. The default value is an
|
||||
// empty VisitorOptions{}.
|
||||
type VisitorOptions struct {
|
||||
// OnlyNumber indicates parser to directly return number value without
|
||||
// conversion, then the first argument of OnInt64 / OnFloat64 will always
|
||||
// be zero.
|
||||
OnlyNumber bool
|
||||
}
|
||||
|
||||
var defaultVisitorOptions = &VisitorOptions{}
|
||||
|
||||
// Preorder decodes the whole JSON string and callbacks each AST node to visitor
|
||||
// during preorder traversal. Any visitor method with an error returned will
|
||||
// break the traversal and the given error will be directly returned. The opts
|
||||
// argument can be reused after every call.
|
||||
func Preorder(str string, visitor Visitor, opts *VisitorOptions) error {
|
||||
if opts == nil {
|
||||
opts = defaultVisitorOptions
|
||||
}
|
||||
// process VisitorOptions first to guarantee that all options will be
|
||||
// constant during decoding and make options more readable.
|
||||
var (
|
||||
optDecodeNumber = !opts.OnlyNumber
|
||||
)
|
||||
|
||||
tv := &traverser{
|
||||
parser: Parser{
|
||||
s: str,
|
||||
noLazy: true,
|
||||
skipValue: false,
|
||||
},
|
||||
visitor: visitor,
|
||||
}
|
||||
|
||||
if optDecodeNumber {
|
||||
tv.parser.decodeNumber(true)
|
||||
}
|
||||
|
||||
err := tv.decodeValue()
|
||||
|
||||
if optDecodeNumber {
|
||||
tv.parser.decodeNumber(false)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
type traverser struct {
|
||||
parser Parser
|
||||
visitor Visitor
|
||||
}
|
||||
|
||||
// NOTE: keep in sync with (*Parser).Parse method.
|
||||
func (self *traverser) decodeValue() error {
|
||||
switch val := self.parser.decodeValue(); val.Vt {
|
||||
case types.V_EOF:
|
||||
return types.ERR_EOF
|
||||
case types.V_NULL:
|
||||
return self.visitor.OnNull()
|
||||
case types.V_TRUE:
|
||||
return self.visitor.OnBool(true)
|
||||
case types.V_FALSE:
|
||||
return self.visitor.OnBool(false)
|
||||
case types.V_STRING:
|
||||
return self.decodeString(val.Iv, val.Ep)
|
||||
case types.V_DOUBLE:
|
||||
return self.visitor.OnFloat64(val.Dv,
|
||||
json.Number(self.parser.s[val.Ep:self.parser.p]))
|
||||
case types.V_INTEGER:
|
||||
return self.visitor.OnInt64(val.Iv,
|
||||
json.Number(self.parser.s[val.Ep:self.parser.p]))
|
||||
case types.V_ARRAY:
|
||||
return self.decodeArray()
|
||||
case types.V_OBJECT:
|
||||
return self.decodeObject()
|
||||
default:
|
||||
return types.ParsingError(-val.Vt)
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: keep in sync with (*Parser).decodeArray method.
|
||||
func (self *traverser) decodeArray() error {
|
||||
sp := self.parser.p
|
||||
ns := len(self.parser.s)
|
||||
|
||||
/* allocate array space and parse every element */
|
||||
if err := self.visitor.OnArrayBegin(_DEFAULT_NODE_CAP); err != nil {
|
||||
if err == VisitOPSkip {
|
||||
// NOTICE: for user needs to skip entry object
|
||||
self.parser.p -= 1
|
||||
if _, e := self.parser.skipFast(); e != 0 {
|
||||
return e
|
||||
}
|
||||
return self.visitor.OnArrayEnd()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
/* check for EOF */
|
||||
self.parser.p = self.parser.lspace(sp)
|
||||
if self.parser.p >= ns {
|
||||
return types.ERR_EOF
|
||||
}
|
||||
|
||||
/* check for empty array */
|
||||
if self.parser.s[self.parser.p] == ']' {
|
||||
self.parser.p++
|
||||
return self.visitor.OnArrayEnd()
|
||||
}
|
||||
|
||||
for {
|
||||
/* decode the value */
|
||||
if err := self.decodeValue(); err != nil {
|
||||
return err
|
||||
}
|
||||
self.parser.p = self.parser.lspace(self.parser.p)
|
||||
|
||||
/* check for EOF */
|
||||
if self.parser.p >= ns {
|
||||
return types.ERR_EOF
|
||||
}
|
||||
|
||||
/* check for the next character */
|
||||
switch self.parser.s[self.parser.p] {
|
||||
case ',':
|
||||
self.parser.p++
|
||||
case ']':
|
||||
self.parser.p++
|
||||
return self.visitor.OnArrayEnd()
|
||||
default:
|
||||
return types.ERR_INVALID_CHAR
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: keep in sync with (*Parser).decodeObject method.
|
||||
func (self *traverser) decodeObject() error {
|
||||
sp := self.parser.p
|
||||
ns := len(self.parser.s)
|
||||
|
||||
/* allocate object space and decode each pair */
|
||||
if err := self.visitor.OnObjectBegin(_DEFAULT_NODE_CAP); err != nil {
|
||||
if err == VisitOPSkip {
|
||||
// NOTICE: for user needs to skip entry object
|
||||
self.parser.p -= 1
|
||||
if _, e := self.parser.skipFast(); e != 0 {
|
||||
return e
|
||||
}
|
||||
return self.visitor.OnObjectEnd()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
/* check for EOF */
|
||||
self.parser.p = self.parser.lspace(sp)
|
||||
if self.parser.p >= ns {
|
||||
return types.ERR_EOF
|
||||
}
|
||||
|
||||
/* check for empty object */
|
||||
if self.parser.s[self.parser.p] == '}' {
|
||||
self.parser.p++
|
||||
return self.visitor.OnObjectEnd()
|
||||
}
|
||||
|
||||
for {
|
||||
var njs types.JsonState
|
||||
var err types.ParsingError
|
||||
|
||||
/* decode the key */
|
||||
if njs = self.parser.decodeValue(); njs.Vt != types.V_STRING {
|
||||
return types.ERR_INVALID_CHAR
|
||||
}
|
||||
|
||||
/* extract the key */
|
||||
idx := self.parser.p - 1
|
||||
key := self.parser.s[njs.Iv:idx]
|
||||
|
||||
/* check for escape sequence */
|
||||
if njs.Ep != -1 {
|
||||
if key, err = unquote.String(key); err != 0 {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := self.visitor.OnObjectKey(key); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
/* expect a ':' delimiter */
|
||||
if err = self.parser.delim(); err != 0 {
|
||||
return err
|
||||
}
|
||||
|
||||
/* decode the value */
|
||||
if err := self.decodeValue(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
self.parser.p = self.parser.lspace(self.parser.p)
|
||||
|
||||
/* check for EOF */
|
||||
if self.parser.p >= ns {
|
||||
return types.ERR_EOF
|
||||
}
|
||||
|
||||
/* check for the next character */
|
||||
switch self.parser.s[self.parser.p] {
|
||||
case ',':
|
||||
self.parser.p++
|
||||
case '}':
|
||||
self.parser.p++
|
||||
return self.visitor.OnObjectEnd()
|
||||
default:
|
||||
return types.ERR_INVALID_CHAR
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: keep in sync with (*Parser).decodeString method.
|
||||
func (self *traverser) decodeString(iv int64, ep int) error {
|
||||
p := self.parser.p - 1
|
||||
s := self.parser.s[iv:p]
|
||||
|
||||
/* fast path: no escape sequence */
|
||||
if ep == -1 {
|
||||
return self.visitor.OnString(s)
|
||||
}
|
||||
|
||||
/* unquote the string */
|
||||
out, err := unquote.String(s)
|
||||
if err != 0 {
|
||||
return err
|
||||
}
|
||||
return self.visitor.OnString(out)
|
||||
}
|
||||
|
||||
// If visitor return this error on `OnObjectBegin()` or `OnArrayBegin()`,
|
||||
// the traverser will skip entry object or array
|
||||
var VisitOPSkip = errors.New("")
|
||||
@@ -0,0 +1,143 @@
|
||||
// +build !amd64,!arm64 go1.26 !go1.17 arm64,!go1.20
|
||||
|
||||
/*
|
||||
* Copyright 2021 ByteDance Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package sonic
|
||||
|
||||
import (
|
||||
`bytes`
|
||||
`encoding/json`
|
||||
`io`
|
||||
`reflect`
|
||||
|
||||
`github.com/bytedance/sonic/option`
|
||||
)
|
||||
|
||||
const apiKind = UseStdJSON
|
||||
|
||||
type frozenConfig struct {
|
||||
Config
|
||||
}
|
||||
|
||||
// Froze convert the Config to API
|
||||
func (cfg Config) Froze() API {
|
||||
api := &frozenConfig{Config: cfg}
|
||||
return api
|
||||
}
|
||||
|
||||
func (cfg frozenConfig) marshalOptions(val interface{}, prefix, indent string) ([]byte, error) {
|
||||
w := bytes.NewBuffer([]byte{})
|
||||
enc := json.NewEncoder(w)
|
||||
enc.SetEscapeHTML(cfg.EscapeHTML)
|
||||
enc.SetIndent(prefix, indent)
|
||||
err := enc.Encode(val)
|
||||
out := w.Bytes()
|
||||
|
||||
// json.Encoder always appends '\n' after encoding,
|
||||
// which is not same with json.Marshal()
|
||||
if len(out) > 0 && out[len(out)-1] == '\n' {
|
||||
out = out[:len(out)-1]
|
||||
}
|
||||
return out, err
|
||||
}
|
||||
|
||||
// Marshal is implemented by sonic
|
||||
func (cfg frozenConfig) Marshal(val interface{}) ([]byte, error) {
|
||||
if !cfg.EscapeHTML {
|
||||
return cfg.marshalOptions(val, "", "")
|
||||
}
|
||||
return json.Marshal(val)
|
||||
}
|
||||
|
||||
// MarshalToString is implemented by sonic
|
||||
func (cfg frozenConfig) MarshalToString(val interface{}) (string, error) {
|
||||
out, err := cfg.Marshal(val)
|
||||
return string(out), err
|
||||
}
|
||||
|
||||
// MarshalIndent is implemented by sonic
|
||||
func (cfg frozenConfig) MarshalIndent(val interface{}, prefix, indent string) ([]byte, error) {
|
||||
if !cfg.EscapeHTML {
|
||||
return cfg.marshalOptions(val, prefix, indent)
|
||||
}
|
||||
return json.MarshalIndent(val, prefix, indent)
|
||||
}
|
||||
|
||||
// UnmarshalFromString is implemented by sonic
|
||||
func (cfg frozenConfig) UnmarshalFromString(buf string, val interface{}) error {
|
||||
r := bytes.NewBufferString(buf)
|
||||
dec := json.NewDecoder(r)
|
||||
if cfg.UseNumber {
|
||||
dec.UseNumber()
|
||||
}
|
||||
if cfg.DisallowUnknownFields {
|
||||
dec.DisallowUnknownFields()
|
||||
}
|
||||
err := dec.Decode(val)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// check the trailing chars
|
||||
offset := dec.InputOffset()
|
||||
if t, err := dec.Token(); !(t == nil && err == io.EOF) {
|
||||
return &json.SyntaxError{ Offset: offset}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Unmarshal is implemented by sonic
|
||||
func (cfg frozenConfig) Unmarshal(buf []byte, val interface{}) error {
|
||||
return cfg.UnmarshalFromString(string(buf), val)
|
||||
}
|
||||
|
||||
// NewEncoder is implemented by sonic
|
||||
func (cfg frozenConfig) NewEncoder(writer io.Writer) Encoder {
|
||||
enc := json.NewEncoder(writer)
|
||||
if !cfg.EscapeHTML {
|
||||
enc.SetEscapeHTML(cfg.EscapeHTML)
|
||||
}
|
||||
return enc
|
||||
}
|
||||
|
||||
// NewDecoder is implemented by sonic
|
||||
func (cfg frozenConfig) NewDecoder(reader io.Reader) Decoder {
|
||||
dec := json.NewDecoder(reader)
|
||||
if cfg.UseNumber {
|
||||
dec.UseNumber()
|
||||
}
|
||||
if cfg.DisallowUnknownFields {
|
||||
dec.DisallowUnknownFields()
|
||||
}
|
||||
return dec
|
||||
}
|
||||
|
||||
// Valid is implemented by sonic
|
||||
func (cfg frozenConfig) Valid(data []byte) bool {
|
||||
return json.Valid(data)
|
||||
}
|
||||
|
||||
// Pretouch compiles vt ahead-of-time to avoid JIT compilation on-the-fly, in
|
||||
// order to reduce the first-hit latency at **amd64** Arch.
|
||||
// Opts are the compile options, for example, "option.WithCompileRecursiveDepth" is
|
||||
// a compile option to set the depth of recursive compile for the nested struct type.
|
||||
// * This is the none implement for !amd64.
|
||||
// It will be useful for someone who develop with !amd64 arch,like Mac M1.
|
||||
func Pretouch(vt reflect.Type, opts ...option.CompileOption) error {
|
||||
return nil
|
||||
}
|
||||
|
||||