Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a5fcf0213 | ||
|
|
3f34a02559 | ||
|
|
a5b67e7657 | ||
|
|
00a44a8e92 | ||
|
|
51b8dddac2 | ||
|
|
d4aece7f1e | ||
|
|
12373e9745 |
No files matched your search
@@ -1,179 +1,409 @@
|
||||
name: Build and Release
|
||||
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:
|
||||
branches: main
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
# Automatically cancel any previous workflow on new push.
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
# ── Go server binaries (6 platform combos) ──────────────────────────────
|
||||
|
||||
build-server:
|
||||
name: Server ${{ matrix.goos }}-${{ matrix.goarch }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# Include amd64 on all platforms.
|
||||
goos: [windows, freebsd, openbsd, linux, dragonfly, darwin]
|
||||
goarch: [amd64, 386]
|
||||
exclude:
|
||||
# Exclude i386 on darwin and dragonfly
|
||||
# because they are not supported by go.
|
||||
- goarch: 386
|
||||
goos: dragonfly
|
||||
- goarch: 386
|
||||
goos: darwin
|
||||
include:
|
||||
# BEIGIN MacOS ARM64
|
||||
- goos: windows
|
||||
goarch: amd64
|
||||
- goos: windows
|
||||
goarch: arm64
|
||||
- goos: linux
|
||||
goarch: amd64
|
||||
- goos: linux
|
||||
goarch: arm64
|
||||
- goos: darwin
|
||||
goarch: amd64
|
||||
- goos: darwin
|
||||
goarch: arm64
|
||||
# END MacOS ARM64
|
||||
# BEGIN Linux ARM 5 6 7
|
||||
- goos: linux
|
||||
goarch: arm
|
||||
goarm: 7
|
||||
- goos: linux
|
||||
goarch: arm
|
||||
goarm: 6
|
||||
- goos: linux
|
||||
goarch: arm
|
||||
goarm: 5
|
||||
# END Linux ARM 5 6 7
|
||||
# BEGIN Android ARM 8
|
||||
- goos: android
|
||||
goarch: arm64
|
||||
# END Android ARM 8
|
||||
# Windows ARM 7
|
||||
- goos: windows
|
||||
goarch: arm
|
||||
goarm: 7
|
||||
# BEGIN Other architectures
|
||||
# BEGIN riscv64 & ARM64
|
||||
- goos: linux
|
||||
goarch: arm64
|
||||
- goos: linux
|
||||
goarch: riscv64
|
||||
# END riscv64 & ARM64
|
||||
# BEGIN MIPS
|
||||
- goos: linux
|
||||
goarch: mips64
|
||||
- goos: linux
|
||||
goarch: mips64le
|
||||
- goos: linux
|
||||
goarch: mipsle
|
||||
- goos: linux
|
||||
goarch: mips
|
||||
# END MIPS
|
||||
# BEGIN PPC
|
||||
- goos: linux
|
||||
goarch: ppc64
|
||||
- goos: linux
|
||||
goarch: ppc64le
|
||||
# END PPC
|
||||
# BEGIN FreeBSD ARM
|
||||
- goos: freebsd
|
||||
goarch: arm64
|
||||
- goos: freebsd
|
||||
goarch: arm
|
||||
goarm: 7
|
||||
# END FreeBSD ARM
|
||||
# BEGIN S390X
|
||||
- goos: linux
|
||||
goarch: s390x
|
||||
# END S390X
|
||||
# BEGIN LoongArch64
|
||||
- goos: linux
|
||||
goarch: loong64
|
||||
# END LoongArch64
|
||||
# END Other architectures
|
||||
# BEGIN OPENBSD ARM
|
||||
- goos: openbsd
|
||||
goarch: arm64
|
||||
- goos: openbsd
|
||||
goarch: arm
|
||||
goarm: 7
|
||||
# END OPENBSD ARM
|
||||
fail-fast: false
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GOOS: ${{ matrix.goos }}
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
GOARM: ${{ matrix.goarm }}
|
||||
CGO_ENABLED: 0
|
||||
steps:
|
||||
- name: Checkout codebase
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Show workflow information
|
||||
id: get_filename
|
||||
run: |
|
||||
export _NAME=$(jq ".[\"$GOOS-$GOARCH$GOARM$GOMIPS\"].friendlyName" -r < .github/build/friendly-filenames.json)
|
||||
echo "GOOS: $GOOS, GOARCH: $GOARCH, GOARM: $GOARM, GOMIPS: $GOMIPS, RELEASE_NAME: $_NAME"
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ^1.23
|
||||
cache: false
|
||||
|
||||
- name: Get project dependencies
|
||||
run: |
|
||||
# we do not cross-compile this.
|
||||
GOOS=linux GOARCH=amd64 GOARM="" GOBIN="$PWD/bin/" go install github.com/jessevdk/go-assets-builder@latest
|
||||
$PWD/bin/go-assets-builder assets -o assets.go
|
||||
# download the modules.
|
||||
go mod download
|
||||
|
||||
- name: Build notebook
|
||||
run: go build -o rz-notebook-${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.goarm }} -ldflags "-X main.NBVERSION=$(git rev-list -1 HEAD)"
|
||||
|
||||
- name: Build Mips softfloat
|
||||
if: matrix.goarch == 'mips' || matrix.goarch == 'mipsle'
|
||||
run: GOMIPS=softfloat go build -o rz-notebook-${{ matrix.goos }}-${{ matrix.goarch }} -ldflags "-X main.NBVERSION=$(git rev-list -1 HEAD)"
|
||||
|
||||
- name: Rename Windows
|
||||
if: matrix.goos == 'windows'
|
||||
run: mv rz-notebook-${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.goarm }} rz-notebook-${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.goarm }}.exe
|
||||
|
||||
- name: Upload files to Artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: rz-notebook-${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.goarm }}
|
||||
path: ./rz-notebook-${{ matrix.goos }}-*
|
||||
|
||||
create_release:
|
||||
name: Create Release
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set variables
|
||||
id: vars
|
||||
run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||
|
||||
- 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: List downloaded files
|
||||
run: ls -lah rz-notebook-*/*
|
||||
|
||||
- name: Create Release
|
||||
if: startsWith(github.ref, 'refs/tags/') || contains(github.ref, 'test-release')
|
||||
uses: softprops/action-gh-release@v2
|
||||
- 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 }}
|
||||
name: Release ${{ github.ref }}
|
||||
tag_name: ${{ steps.meta.outputs.tag }}
|
||||
name: ${{ steps.meta.outputs.name }}
|
||||
target_commitish: ${{ steps.meta.outputs.target_commitish }}
|
||||
generate_release_notes: true
|
||||
draft: true
|
||||
prerelease: false
|
||||
files: |
|
||||
./rz-notebook-*/*
|
||||
|
||||
- name: Create Nightly
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
name: Nightly (${{ steps.vars.outputs.sha_short }})
|
||||
draft: false
|
||||
tag_name: nightly-${{ steps.vars.outputs.sha_short }}
|
||||
generate_release_notes: true
|
||||
prerelease: true
|
||||
prerelease: ${{ steps.meta.outputs.prerelease == 'true' }}
|
||||
files: |
|
||||
./rz-notebook-*/*
|
||||
rizin-notebook-linux-*/*
|
||||
rizin-notebook-windows-*/*
|
||||
rizin-notebook-darwin-*/*
|
||||
rizin-notebook-plugin-*/*
|
||||
cutter-notebook-plugin-*/*
|
||||
plugin-sources/*
|
||||
release-checksums/*
|
||||
@@ -1,44 +0,0 @@
|
||||
name: Build Notebook
|
||||
on:
|
||||
pull_request:
|
||||
branches: main
|
||||
|
||||
# Automatically cancel any previous workflow on new push.
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
steps:
|
||||
- name: Checkout codebase
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ^1.23
|
||||
cache: false
|
||||
|
||||
- name: Get project dependencies
|
||||
run: |
|
||||
# we do not cross-compile this.
|
||||
GOOS=linux GOARCH=amd64 GOARM="" GOBIN="$PWD/bin/" go install github.com/jessevdk/go-assets-builder@latest
|
||||
$PWD/bin/go-assets-builder assets -o assets.go
|
||||
# download the modules.
|
||||
go mod download
|
||||
|
||||
- name: Build notebook
|
||||
run: go build -o rz-notebook -ldflags "-X main.NBVERSION=$(git rev-list -1 HEAD)"
|
||||
|
||||
- name: Upload files to Artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: rz-notebook
|
||||
path: ./rz-notebook
|
||||
@@ -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,3 +1,47 @@
|
||||
rz-notebook
|
||||
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
|
||||
# required for go-assets-builder
|
||||
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,176 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/jessevdk/go-assets"
|
||||
)
|
||||
|
||||
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 _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 _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 _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: 10px; }\n </style>\n</head>\n<body>\n <pre>{{ .output | raw }}</pre>\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 _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 _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 @font-face { font-family: 'Inconsolata'; src: URL('{{ .root }}static/inconsolata.ttf') format('truetype'); }\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 textarea {\n margin: 5px;\n font-family: Inconsolata;\n background-color: inherit;\n color: inherit;\n line-height: 8px;\n resize: none;\n }\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 newsc() {\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.style.height = \"300px\";\n iframe.setAttribute(\"frameborder\", \"0\");\n iframe.setAttribute(\"marginheight\", \"0\");\n iframe.setAttribute(\"marginwidth\", \"0\");\n iframe.setAttribute(\"width\", \"100%\");\n iframe.setAttribute(\"height\", \"110%\");\n iframe.setAttribute(\"scrolling\", \"no\");\n iframe.setAttribute(\"onload\", \"output(this)\");\n iframe.src = \"{{ $root }}output/input/script/{{ $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/console/{{ $page.unique Line 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 _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 _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 _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 _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 _Assets3b75d230b7255e19d794d5bc539934f214d7ec26 = ".code-editor, .code-lines {\n font-family: lucida console, courier new, courier, monospace;\n margin: 0;\n height: 75vh;\n border-radius: 0;\n resize: none;\n line-height: 1.2;\n outline: none;\n -moz-box-sizing: border-box;\n -webkit-box-sizing: border-box;\n box-sizing: border-box;\n font-family: Inconsolata;\n}\n.code-lines:focus-visible,\n.code-editor:focus-visible {\n outline:none;\n}\n.code-lines {\n display: flex;\n border-color: transparent;\n overflow-y: hidden;\n text-align: right;\n box-shadow: none;\n color: #707070;\n background-color: #d8d8d8;\n position: absolute;\n width: 3.5rem;\n background-color:#3E3D32;\n border-color:#3E3D32;\n color:#928869;\n}\n.code-editor {\n padding-left: calc(3.5rem + 5px);\n width:100%;\n background-color:#272822;\n border-color:#272822;\n color:#ffffff;\n}"
|
||||
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
|
||||
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 _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 _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 _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 _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 _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 _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 _Assets0010c486fe281ee7c3c324d04b8dfecc97bcf281 = "<!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 @font-face { font-family: 'Inconsolata'; src: URL('{{ .root }}static/inconsolata.ttf') format('truetype'); }\n </style>\n <link rel=\"stylesheet\" href=\"{{ .root }}static/codelines.css\">\n</head>\n<body>\n <form style=\"margin-top: 15px; height: 100%;\" action=\"{{ .root }}output/script/{{ .unique }}\" method=\"POST\">\n <div class=\"form-group\">\n <p><textarea id=\"codelines\" class=\"code-lines\" wrap=\"off\" readonly>1 </textarea><textarea id=\"codeeditor\" class=\"code-editor\" wrap=\"off\" name=\"script\">console.log(rizin.cmd(\"?e hello world\").trim(), \"from the script\");</textarea></p>\n </div>\n <div class=\"form-group\">\n <button class=\"btn btn-primary\" type=\"submit\">Run</button>\n <a class=\"btn\" href=\"{{ .root }}output/deleted\">Cancel</a>\n </div>\n </form>\n <script type=\"text/javascript\">\n var editor = document.getElementById('codeeditor');\n var lines = document.getElementById('codelines');\n editor.addEventListener('scroll', function() {\n lines.scrollTop = editor.scrollTop;\n lines.scrollLeft = editor.scrollLeft;\n });\n editor.addEventListener('keydown', function(e) {\n if (e.keyCode === 9) {\n // TAB = 9\n e.preventDefault();\n editor.value = editor.value.slice(0, editor.selectionStart) + '\\t' + value.slice(editor.selectionEnd);\n editor.setSelectionRange(editor.selectionStart + 2, editor.selectionStart + 2)\n }\n });\n var lineCountCache = 0;\n function updateLines() {\n var editorLines = editor.value.split('\\n');\n if (lineCountCache != editorLines.length) {\n lines.value = editorLines.map(function(e, n) {\n return (n + 1).toString() + ' ';\n }).join('\\n');\n }\n lineCountCache = lineCount;\n }\n editor.addEventListener('input', function() {\n updateLines();\n });\n </script>\n</body>\n</html>"
|
||||
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 _Assetsb45a00d76b7dcbaa3b9c30ab2b2d16404478b419 = "{{ $error := .error }}{{ $llen := 0 }}{{ if .location }}{{ $llen := len .location }}{{ end }}<!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>"
|
||||
|
||||
// Assets returns go-assets FileSystem
|
||||
var Assets = assets.NewFileSystem(map[string][]string{"/": []string{"assets"}, "/assets": []string{}, "/assets/static": []string{"codelines.css", "spectre.min.css", "jquery.slim.min.js", "inconsolata.ttf", "simplemde.min.css", "logo.png", "spectre-icons.min.css", "simplemde.min.js", "favicon.ico", "rizin-favicon.ico"}, "/assets/templates": []string{"about.tmpl", "markdown-edit.tmpl", "settings-edit.tmpl", "index.tmpl", "script.tmpl", "page-view.tmpl", "console.tmpl", "settings.tmpl", "output.tmpl", "reload.tmpl", "page-new.tmpl", "console-error.tmpl", "error.tmpl", "markdown-view.tmpl"}}, map[string]*assets.File{
|
||||
"/assets/templates/markdown-view.tmpl": &assets.File{
|
||||
Path: "/assets/templates/markdown-view.tmpl",
|
||||
FileMode: 0x1a4,
|
||||
Mtime: time.Unix(1637028787, 1637028787256874628),
|
||||
Data: []byte(_Assetse3765268b98f9d73e1fee1dfddb9c8e4001d6f7a),
|
||||
}, "/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/static/logo.png": &assets.File{
|
||||
Path: "/assets/static/logo.png",
|
||||
FileMode: 0x1a4,
|
||||
Mtime: time.Unix(1611354557, 1611354557771466726),
|
||||
Data: []byte(_Assetse134fa82eca7c17da76f3698c373a24c1feacbdd),
|
||||
}, "/assets/static/rizin-favicon.ico": &assets.File{
|
||||
Path: "/assets/static/rizin-favicon.ico",
|
||||
FileMode: 0x1a4,
|
||||
Mtime: time.Unix(1611354557, 1611354557771466726),
|
||||
Data: []byte(_Assetsaad1e392c9fe33a13d63fefdb59493ea12de3f06),
|
||||
}, "/assets/templates/console-error.tmpl": &assets.File{
|
||||
Path: "/assets/templates/console-error.tmpl",
|
||||
FileMode: 0x1a4,
|
||||
Mtime: time.Unix(1637028787, 1637028787256874628),
|
||||
Data: []byte(_Assets92ed65f24ecf383f92d30c1d5413460a473ae582),
|
||||
}, "/assets/static/codelines.css": &assets.File{
|
||||
Path: "/assets/static/codelines.css",
|
||||
FileMode: 0x1a4,
|
||||
Mtime: time.Unix(1648424124, 1648424124487520024),
|
||||
Data: []byte(_Assets3b75d230b7255e19d794d5bc539934f214d7ec26),
|
||||
}, "/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/templates/settings-edit.tmpl": &assets.File{
|
||||
Path: "/assets/templates/settings-edit.tmpl",
|
||||
FileMode: 0x1a4,
|
||||
Mtime: time.Unix(1648403752, 1648403752655773019),
|
||||
Data: []byte(_Assets463d1a70a0b97ff5743af8f39e2c5bb780f845bd),
|
||||
}, "/assets/templates/reload.tmpl": &assets.File{
|
||||
Path: "/assets/templates/reload.tmpl",
|
||||
FileMode: 0x1a4,
|
||||
Mtime: time.Unix(1637028787, 1637028787256874628),
|
||||
Data: []byte(_Assets6947bea9c2e7bfbff103ea8688234dfb7ab7d9d0),
|
||||
}, "/assets/static": &assets.File{
|
||||
Path: "/assets/static",
|
||||
FileMode: 0x800001ed,
|
||||
Mtime: time.Unix(1648424124, 1648424124487520024),
|
||||
Data: nil,
|
||||
}, "/assets/static/inconsolata.ttf": &assets.File{
|
||||
Path: "/assets/static/inconsolata.ttf",
|
||||
FileMode: 0x1a4,
|
||||
Mtime: time.Unix(1637028787, 1637028787256874628),
|
||||
Data: []byte(_Assets8fdbadd31c8f0302966785b472dbe1cdec02f9b4),
|
||||
}, "/assets/static/simplemde.min.js": &assets.File{
|
||||
Path: "/assets/static/simplemde.min.js",
|
||||
FileMode: 0x1a4,
|
||||
Mtime: time.Unix(1611354557, 1611354557771466726),
|
||||
Data: []byte(_Assets25ed14e5f6bcc6e42a5e6dee7c116155e2480e3b),
|
||||
}, "/assets/templates/about.tmpl": &assets.File{
|
||||
Path: "/assets/templates/about.tmpl",
|
||||
FileMode: 0x1a4,
|
||||
Mtime: time.Unix(1637028787, 1637028787256874628),
|
||||
Data: []byte(_Assets52de3ee91e954b604e6683f45a4970540bf37631),
|
||||
}, "/assets/templates/markdown-edit.tmpl": &assets.File{
|
||||
Path: "/assets/templates/markdown-edit.tmpl",
|
||||
FileMode: 0x1a4,
|
||||
Mtime: time.Unix(1637028787, 1637028787256874628),
|
||||
Data: []byte(_Assets25265b46fc5005cf89577b3590eb083ef336b559),
|
||||
}, "/assets/static/favicon.ico": &assets.File{
|
||||
Path: "/assets/static/favicon.ico",
|
||||
FileMode: 0x1a4,
|
||||
Mtime: time.Unix(1611354557, 1611354557768133350),
|
||||
Data: []byte(_Assetsab269208dc370b8abed2a667e16e03ca0567fade),
|
||||
}, "/assets/templates/script.tmpl": &assets.File{
|
||||
Path: "/assets/templates/script.tmpl",
|
||||
FileMode: 0x1a4,
|
||||
Mtime: time.Unix(1648424124, 1648424124487520024),
|
||||
Data: []byte(_Assets0010c486fe281ee7c3c324d04b8dfecc97bcf281),
|
||||
}, "/assets/templates/console.tmpl": &assets.File{
|
||||
Path: "/assets/templates/console.tmpl",
|
||||
FileMode: 0x1a4,
|
||||
Mtime: time.Unix(1637028787, 1637028787256874628),
|
||||
Data: []byte(_Assetsbfdf4a3e904014e3d8fb363c3d1206b6f5180288),
|
||||
}, "/assets/templates/error.tmpl": &assets.File{
|
||||
Path: "/assets/templates/error.tmpl",
|
||||
FileMode: 0x1a4,
|
||||
Mtime: time.Unix(1683428532, 1683428532860768932),
|
||||
Data: []byte(_Assetsb45a00d76b7dcbaa3b9c30ab2b2d16404478b419),
|
||||
}, "/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(1648424124, 1648424124487520024),
|
||||
Data: nil,
|
||||
}, "/assets/templates/index.tmpl": &assets.File{
|
||||
Path: "/assets/templates/index.tmpl",
|
||||
FileMode: 0x1a4,
|
||||
Mtime: time.Unix(1637028787, 1637028787256874628),
|
||||
Data: []byte(_Assetsa73f174e8addbc8adb067ef8cce595d0ae511369),
|
||||
}, "/assets/templates/settings.tmpl": &assets.File{
|
||||
Path: "/assets/templates/settings.tmpl",
|
||||
FileMode: 0x1a4,
|
||||
Mtime: time.Unix(1648403752, 1648403752655773019),
|
||||
Data: []byte(_Assetsebfcd4e56920358b26e898adc7fbb3022afd8a3e),
|
||||
}, "/assets/templates/output.tmpl": &assets.File{
|
||||
Path: "/assets/templates/output.tmpl",
|
||||
FileMode: 0x1a4,
|
||||
Mtime: time.Unix(1648424124, 1648424124487520024),
|
||||
Data: []byte(_Assets217fba07788194e5eb162b7794a0b9d7622d092c),
|
||||
}, "/assets/templates/page-new.tmpl": &assets.File{
|
||||
Path: "/assets/templates/page-new.tmpl",
|
||||
FileMode: 0x1a4,
|
||||
Mtime: time.Unix(1637028787, 1637028787256874628),
|
||||
Data: []byte(_Assets8d74849a5560d3b62ca6202780224621d98ab7c3),
|
||||
}, "/": &assets.File{
|
||||
Path: "/",
|
||||
FileMode: 0x800001ed,
|
||||
Mtime: time.Unix(1683428729, 1683428729046641715),
|
||||
Data: nil,
|
||||
}, "/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/spectre.min.css": &assets.File{
|
||||
Path: "/assets/static/spectre.min.css",
|
||||
FileMode: 0x1a4,
|
||||
Mtime: time.Unix(1611354557, 1611354557771466726),
|
||||
Data: []byte(_Assetscbf8cb58af475c51e110eb6ea4f06b938a70a87b),
|
||||
}, "/assets/templates/page-view.tmpl": &assets.File{
|
||||
Path: "/assets/templates/page-view.tmpl",
|
||||
FileMode: 0x1a4,
|
||||
Mtime: time.Unix(1648424124, 1648424124487520024),
|
||||
Data: []byte(_Assetsccaa111ee439fc8a890b718441ba429bfa47c126),
|
||||
}}, "")
|
||||
@@ -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,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 := 0 }}{{ if .location }}{{ $llen := len .location }}{{ end }}<!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>
|
||||
|
||||
@@ -22,9 +22,247 @@
|
||||
@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: 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,46 +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">
|
||||
@font-face { font-family: 'Inconsolata'; src: URL('{{ .root }}static/inconsolata.ttf') format('truetype'); }
|
||||
.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; }
|
||||
textarea {
|
||||
margin: 5px;
|
||||
font-family: Inconsolata;
|
||||
background-color: inherit;
|
||||
color: inherit;
|
||||
line-height: 8px;
|
||||
resize: none;
|
||||
@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);
|
||||
}
|
||||
}
|
||||
</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) {
|
||||
obj.style.height = (obj.contentWindow.document.documentElement.scrollHeight + 20) + 'px';
|
||||
if (obj.contentWindow.location.href.indexOf("/deleted") > 0) {
|
||||
obj.parentNode.parentNode.removeChild(obj.parentNode);
|
||||
}
|
||||
window.scrollTo(0, document.body.scrollHeight);
|
||||
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");
|
||||
@@ -58,8 +437,8 @@
|
||||
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 }}";
|
||||
@@ -78,7 +457,6 @@
|
||||
iframe.setAttribute("marginheight", "0");
|
||||
iframe.setAttribute("marginwidth", "0");
|
||||
iframe.setAttribute("width", "100%");
|
||||
iframe.setAttribute("height", "110%");
|
||||
iframe.setAttribute("scrolling", "no");
|
||||
iframe.setAttribute("onload", "output(this)");
|
||||
iframe.src = "{{ $root }}output/input/script/{{ $page.unique }}";
|
||||
@@ -92,11 +470,11 @@
|
||||
|
||||
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/console/{{ $page.unique }}";
|
||||
@@ -108,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>
|
||||
@@ -120,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>
|
||||
@@ -131,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>
|
||||
@@ -143,37 +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">
|
||||
{{ if eq $line.type "script" }}
|
||||
<code style="display: block; white-space: pre-wrap;" class="input-group-addon spacing-right spacing-left">{{ $line.script }}</code>
|
||||
<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 class="input-group-addon spacing-right spacing-left">{{ $line.command }}</code>
|
||||
<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 }}
|
||||
<a class="btn float-right" href="{{ $root }}output/delete/{{ $page.unique }}/{{ $line.unique }}" onclick="confirm_delete(event)">
|
||||
<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()"><i class="icon icon-share">Script</i> Script</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>
|
||||
@@ -13,20 +13,38 @@
|
||||
@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">console.log(rizin.cmd("?e hello world").trim(), "from the script");</textarea></p>
|
||||
<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>
|
||||
<a class="btn" href="{{ .root }}output/deleted">Cancel</a>
|
||||
<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;
|
||||
@@ -35,7 +53,7 @@
|
||||
if (e.keyCode === 9) {
|
||||
// TAB = 9
|
||||
e.preventDefault();
|
||||
editor.value = editor.value.slice(0, editor.selectionStart) + '\t' + value.slice(editor.selectionEnd);
|
||||
editor.value = editor.value.slice(0, editor.selectionStart) + '\t' + editor.value.slice(editor.selectionEnd);
|
||||
editor.setSelectionRange(editor.selectionStart + 2, editor.selectionStart + 2)
|
||||
}
|
||||
});
|
||||
@@ -47,11 +65,31 @@
|
||||
return (n + 1).toString() + ' ';
|
||||
}).join('\n');
|
||||
}
|
||||
lineCountCache = lineCount;
|
||||
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 |
@@ -1,44 +1,57 @@
|
||||
module github.com/rizinorg/rizin-notebook
|
||||
|
||||
go 1.22
|
||||
go 1.24.0
|
||||
|
||||
toolchain go1.23.3
|
||||
toolchain go1.24.1
|
||||
|
||||
require (
|
||||
github.com/dop251/goja v0.0.0-20241024094426-79f3a7efcdbd
|
||||
github.com/gin-gonic/gin v1.10.0
|
||||
github.com/jessevdk/go-assets v0.0.0-20160921144138-4f4301a06e15
|
||||
golang.org/x/sync v0.10.0
|
||||
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/sonic v1.12.5 // indirect
|
||||
github.com/bytedance/sonic/loader v0.2.1 // indirect
|
||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.4 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.7 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
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.23.0 // 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.4 // indirect
|
||||
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // 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.2.9 // 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/pelletier/go-toml/v2 v2.2.3 // 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.2.12 // indirect
|
||||
golang.org/x/arch v0.12.0 // indirect
|
||||
golang.org/x/crypto v0.31.0 // indirect
|
||||
golang.org/x/net v0.32.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
google.golang.org/protobuf v1.35.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.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
|
||||
)
|
||||
@@ -1,52 +1,55 @@
|
||||
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/sonic v1.12.5 h1:hoZxY8uW+mT+OpkcUWw4k0fDINtOcVavEsGfzwzFU/w=
|
||||
github.com/bytedance/sonic v1.12.5/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/bytedance/sonic/loader v0.2.1 h1:1GgorWTqf12TA8mma4DDSbaQigE2wOgQo7iCjjJv3+E=
|
||||
github.com/bytedance/sonic/loader v0.2.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
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.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
|
||||
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dop251/goja v0.0.0-20241024094426-79f3a7efcdbd h1:QMSNEh9uQkDjyPwu/J541GgSH+4hw+0skJDIj9HJ3mE=
|
||||
github.com/dop251/goja v0.0.0-20241024094426-79f3a7efcdbd/go.mod h1:MxLav0peU43GgvwVgNbLAj1s/bSGboKkhuULvq/7hx4=
|
||||
github.com/gabriel-vasile/mimetype v1.4.7 h1:SKFKl7kD0RiPdbht0s7hFtjl489WcQ1VyPW8ZzUMYCA=
|
||||
github.com/gabriel-vasile/mimetype v1.4.7/go.mod h1:GDlAgAyIRT27BhFl53XNAFtfjzOkLaF35JdEG0P7LtU=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
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.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o=
|
||||
github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
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.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM=
|
||||
github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
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-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg=
|
||||
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
|
||||
github.com/jessevdk/go-assets v0.0.0-20160921144138-4f4301a06e15 h1:cW/amwGEJK5MSKntPXRjX4dxs/nGxGT8gXKIsKFmHGc=
|
||||
github.com/jessevdk/go-assets v0.0.0-20160921144138-4f4301a06e15/go.mod h1:Fdm/oWRW+CH8PRbLntksCNtmcCBximKPkVQYvmMl80k=
|
||||
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.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
|
||||
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
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=
|
||||
@@ -56,46 +59,87 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
|
||||
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/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
||||
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.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
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.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
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.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
golang.org/x/arch v0.12.0 h1:UsYJhbzPYGsT0HbEdmYcqtCv8UNGvnaL561NnIUvaKg=
|
||||
golang.org/x/arch v0.12.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI=
|
||||
golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs=
|
||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
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.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io=
|
||||
google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
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=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
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,384 +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
|
||||
|
||||
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)
|
||||
}
|
||||
jsvm := NewJavaScript()
|
||||
if jsvm == nil {
|
||||
panic("failed to create scripting engine")
|
||||
}
|
||||
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) newscript(unique, script 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": "script",
|
||||
"unique": eunique,
|
||||
"script": script,
|
||||
})
|
||||
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)
|
||||
}
|
||||
@@ -3,40 +3,144 @@ 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 {
|
||||
@@ -55,35 +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)
|
||||
}
|
||||
buf = string(bytes.Trim([]byte(buf), "\x00"))
|
||||
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
|
||||
@@ -4,22 +4,30 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
"golang.org/x/sync/semaphore"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 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 string
|
||||
output strings.Builder
|
||||
}
|
||||
|
||||
func rizinCmd(command string, rizin *Rizin) (string, error) {
|
||||
rizin.exec("scr.color=0")
|
||||
result, err := rizin.exec(command)
|
||||
rizin.exec("scr.color=3")
|
||||
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
|
||||
}
|
||||
|
||||
@@ -39,83 +47,131 @@ func convertValue(ivalue interface{}) string {
|
||||
func NewJavaScript() *JavaScript {
|
||||
runtime := goja.New()
|
||||
if runtime == nil {
|
||||
fmt.Println("cannot create a JavaScript runtime.")
|
||||
fmt.Println("error: cannot create JavaScript runtime")
|
||||
return nil
|
||||
}
|
||||
|
||||
sem := semaphore.NewWeighted(1)
|
||||
js := &JavaScript{semaphore: sem, runtime: runtime, rizin: nil}
|
||||
rizin := map[string]interface{}{}
|
||||
rizin["cmd"] = func(args ...interface{}) goja.Value {
|
||||
|
||||
// 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."))
|
||||
} else if len(args) < 1 {
|
||||
}
|
||||
if len(args) < 1 {
|
||||
panic(js.runtime.ToValue("No string was passed."))
|
||||
}
|
||||
switch value := args[0].(type) {
|
||||
case string:
|
||||
result, err := rizinCmd(value, js.rizin)
|
||||
if err != nil {
|
||||
panic(js.runtime.ToValue(err))
|
||||
}
|
||||
return js.runtime.ToValue(result)
|
||||
default:
|
||||
cmdStr, ok := args[0].(string)
|
||||
if !ok {
|
||||
panic(js.runtime.ToValue("input is not a string."))
|
||||
}
|
||||
}
|
||||
rizin["cmdj"] = func(args ...interface{}) goja.Value {
|
||||
if js.rizin == nil {
|
||||
panic(js.runtime.ToValue("Rizin pipe is closed."))
|
||||
} else if len(args) < 1 {
|
||||
panic(js.runtime.ToValue("No string was passed."))
|
||||
}
|
||||
switch value := args[0].(type) {
|
||||
case string:
|
||||
result, err := rizinCmd(value, js.rizin)
|
||||
if err != nil {
|
||||
panic(js.runtime.ToValue(err))
|
||||
}
|
||||
var data interface{}
|
||||
err = json.Unmarshal([]byte(result), &data)
|
||||
if err != nil {
|
||||
panic(js.runtime.ToValue(err))
|
||||
}
|
||||
return js.runtime.ToValue(data)
|
||||
default:
|
||||
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)
|
||||
}
|
||||
|
||||
console := map[string]interface{}{}
|
||||
console["log"] = func(args ...interface{}) {
|
||||
n_args := len(args)
|
||||
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 += convertValue(value)
|
||||
if i+1 < n_args {
|
||||
js.output += " "
|
||||
js.output.WriteString(convertValue(value))
|
||||
if i+1 < len(args) {
|
||||
js.output.WriteString(" ")
|
||||
}
|
||||
}
|
||||
js.output += "\n"
|
||||
js.output.WriteString("\n")
|
||||
}
|
||||
|
||||
runtime.Set("rizin", rizin)
|
||||
runtime.Set("console", console)
|
||||
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, rizin *Rizin) (string, error) {
|
||||
func (js *JavaScript) exec(script string, rz *Rizin) (string, error) {
|
||||
if !js.semaphore.TryAcquire(1) {
|
||||
return "", errors.New("A script is already running.")
|
||||
return "", errors.New("a script is already running")
|
||||
}
|
||||
defer js.semaphore.Release(1)
|
||||
js.rizin = rizin
|
||||
js.output = ""
|
||||
timer := time.AfterFunc(5*time.Minute, func() {
|
||||
|
||||
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
|
||||
result := js.output.String()
|
||||
|
||||
// Clean up state for next execution.
|
||||
js.rizin = nil
|
||||
js.output = ""
|
||||
timer.Stop()
|
||||
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,175 +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.")
|
||||
})
|
||||
output.GET("/loaded", func(c *gin.Context) {
|
||||
c.String(200, "found.")
|
||||
})
|
||||
output.POST("/script/:unique", func(c *gin.Context) {
|
||||
unique := c.Param("unique")
|
||||
script := c.DefaultPostForm("script", "")
|
||||
if !IsValidNonce(unique) || len(script) < 1 {
|
||||
c.HTML(400, "console-error.tmpl", gin.H{
|
||||
"error": "invalid request",
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
section := notebook.newscript(unique, script)
|
||||
if len(section) < 1 {
|
||||
c.HTML(400, "console-error.tmpl", gin.H{
|
||||
"error": "cannot create section for output.",
|
||||
"root": webroot,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
rizin := notebook.open(unique, false)
|
||||
jsvm := notebook.jsvm
|
||||
go func(unique, name, script string, jsvm *JavaScript, rizin *Rizin) {
|
||||
output, err := jsvm.exec(script, rizin)
|
||||
if err != nil {
|
||||
errstr := strings.Replace(err.Error(), " at main.NewJavaScript.func1 (native)", "", 1)
|
||||
output += "\nException: " + errstr
|
||||
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))
|
||||
}
|
||||
notebook.save([]byte(output), unique, name+".out")
|
||||
}(unique, section, script, jsvm, rizin)
|
||||
|
||||
c.Redirect(302, webroot+"output/check/"+unique+"/"+section)
|
||||
})
|
||||
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",
|
||||
"root": webroot,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
rizin := notebook.open(unique, false)
|
||||
if rizin == nil {
|
||||
c.HTML(400, "console-error.tmpl", gin.H{
|
||||
"error": "pipe is closed.",
|
||||
"root": webroot,
|
||||
})
|
||||
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,
|
||||
})
|
||||
if cell.Executed > 0 {
|
||||
actionTime = formatActionTime(cell.Executed)
|
||||
} else {
|
||||
c.Redirect(302, webroot+"output/loaded")
|
||||
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("/input/script/:unique", func(c *gin.Context) {
|
||||
unique := c.Param("unique")
|
||||
if !IsValidNonce(unique) {
|
||||
c.HTML(400, "error.tmpl", gin.H{
|
||||
|
||||
// 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 request",
|
||||
})
|
||||
} else {
|
||||
c.HTML(200, "script.tmpl", gin.H{
|
||||
"unique": unique,
|
||||
"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) {
|
||||
c.HTML(400, "error.tmpl", gin.H{
|
||||
if !IsValidNonce(unique, PageNonceSize) {
|
||||
c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"error": "invalid request",
|
||||
"error": "invalid page identifier",
|
||||
})
|
||||
} else {
|
||||
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")
|
||||
if !IsValidNonce(unique, PageNonceSize) {
|
||||
c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{
|
||||
"root": webroot,
|
||||
"error": "invalid page identifier",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
@@ -175,3 +175,27 @@
|
||||
|
||||
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
|
||||
@@ -47,7 +47,7 @@ junit.xml
|
||||
ast/test.out
|
||||
ast/bench.sh
|
||||
|
||||
!testdata/*.json.gz
|
||||
!testdata/**/*.json.gz
|
||||
fuzz/testdata
|
||||
*__debug_bin*
|
||||
*pprof
|
||||
|
||||
@@ -4,3 +4,6 @@
|
||||
[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
|
||||
@@ -6,9 +6,10 @@ A blazingly fast JSON serializing & deserializing library, accelerated by JI
|
||||
|
||||
## Requirement
|
||||
|
||||
- Go: 1.17~1.23
|
||||
- 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)
|
||||
- CPU: AMD64 / (ARM64, need go1.20 above)
|
||||
|
||||
## Features
|
||||
|
||||
@@ -211,7 +212,7 @@ 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 **DONOT** compacting them for performance concerns. We provide the option `encoder.CompactMarshaler` to add compacting process.
|
||||
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
|
||||
|
||||
@@ -384,12 +385,12 @@ See [ast/visitor.go](https://github.com/bytedance/sonic/blob/main/ast/visitor.go
|
||||
|
||||
## Compatibility
|
||||
|
||||
For developers who want to use sonic to meet diffirent scenarios, we provide some integrated configs as `sonic.API`
|
||||
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 beflow configs will all equal to `ConfigStd`.
|
||||
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
|
||||
|
||||
@@ -480,9 +481,9 @@ But `ast.Visitor` is not a very handy API. You might need to write a lot of code
|
||||
|
||||
### Buffer Size
|
||||
|
||||
Sonic use memory pool in many places like `encoder.Encode`, `ast.Node.MarshalJSON` to improve performace, 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.
|
||||
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
|
||||
### 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:
|
||||
|
||||
@@ -490,6 +491,11 @@ For security, sonic use [FSM](native/skip_one.c) algorithm to validate JSON whe
|
||||
- `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.
|
||||
@@ -6,9 +6,10 @@
|
||||
|
||||
## 依赖
|
||||
|
||||
- Go: 1.17~1.23
|
||||
- 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 以上)
|
||||
- CPU: AMD64 / (ARM64, 需要 Go1.20 以上)
|
||||
|
||||
## 接口
|
||||
|
||||
@@ -286,9 +287,9 @@ sub := root.Get("key3").Index(2).Int64() // == 3
|
||||
|
||||
`ast.Searcher`提供了一些选项,以满足用户的不同需求:
|
||||
|
||||
```
|
||||
opts:= ast.SearchOption{CopyReturn: true…}
|
||||
Val, err:= sonic。gettwithoptions (JSON, opts, "key")
|
||||
```go
|
||||
opts := ast.SearchOption{CopyReturn: true…}
|
||||
val, err := sonic.GetWithOptions(JSON, opts, "key")
|
||||
```
|
||||
|
||||
- CopyReturn
|
||||
@@ -389,7 +390,7 @@ type Visitor interface {
|
||||
- `ConfigDefault`: sonic的默认配置 (`EscapeHTML=false`, `SortKeys=false`…) 保证性能同时兼顾安全性。
|
||||
- `ConfigStd`: 与 `encoding/json` 保证完全兼容的配置
|
||||
- `ConfigFastest`: 最快的配置(`NoQuoteTextMarshaler=true...`) 保证性能最优但是会缺少一些安全性检查(validate UTF8 等)
|
||||
Sonic **不**确保支持所有环境,由于开发高性能代码的困难。在不支持声音的环境中,实现将回落到 `encoding/json`。因此上述配置将全部等于`ConfigStd`。
|
||||
Sonic **不**确保支持所有环境,由于开发高性能代码的困难。在不支持sonic的环境中,实现将回落到 `encoding/json`。因此上述配置将全部等于`ConfigStd`。
|
||||
|
||||
## 注意事项
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ type Config struct {
|
||||
// CopyString indicates decoder to decode string values by copying instead of referring.
|
||||
CopyString bool
|
||||
|
||||
// ValidateString indicates decoder and encoder to valid string values: decoder will return errors
|
||||
// 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
|
||||
|
||||
@@ -94,6 +94,9 @@ type Config struct {
|
||||
|
||||
// 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 (
|
||||
@@ -111,7 +114,6 @@ var (
|
||||
|
||||
// ConfigFastest is the fastest config of APIs, aiming at speed.
|
||||
ConfigFastest = Config{
|
||||
NoQuoteTextMarshaler: true,
|
||||
NoValidateJSONMarshaler: true,
|
||||
NoValidateJSONSkip: true,
|
||||
}.Froze()
|
||||
@@ -120,7 +122,7 @@ var (
|
||||
|
||||
// API is a binding of specific config.
|
||||
// This interface is inspired by github.com/json-iterator/go,
|
||||
// and has same behaviors under equavilent config.
|
||||
// and has same behaviors under equivalent config.
|
||||
type API interface {
|
||||
// MarshalToString returns the JSON encoding string of v
|
||||
MarshalToString(v interface{}) (string, error)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//go:build (amd64 && go1.17 && !go1.24) || (arm64 && go1.20 && !go1.24)
|
||||
// +build amd64,go1.17,!go1.24 arm64,go1.20,!go1.24
|
||||
//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.
|
||||
@@ -20,62 +20,21 @@
|
||||
package ast
|
||||
|
||||
import (
|
||||
`runtime`
|
||||
`unsafe`
|
||||
"runtime"
|
||||
"unsafe"
|
||||
|
||||
`github.com/bytedance/sonic/encoder`
|
||||
`github.com/bytedance/sonic/internal/native`
|
||||
`github.com/bytedance/sonic/internal/native/types`
|
||||
`github.com/bytedance/sonic/internal/rt`
|
||||
uq `github.com/bytedance/sonic/unquote`
|
||||
`github.com/bytedance/sonic/utf8`
|
||||
"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
|
||||
|
||||
//go:nocheckptr
|
||||
func quote(buf *[]byte, val string) {
|
||||
*buf = append(*buf, '"')
|
||||
if len(val) == 0 {
|
||||
*buf = append(*buf, '"')
|
||||
return
|
||||
}
|
||||
|
||||
sp := rt.IndexChar(val, 0)
|
||||
nb := len(val)
|
||||
b := (*rt.GoSlice)(unsafe.Pointer(buf))
|
||||
|
||||
// input buffer
|
||||
for nb > 0 {
|
||||
// output buffer
|
||||
dp := unsafe.Pointer(uintptr(b.Ptr) + uintptr(b.Len))
|
||||
dn := b.Cap - b.Len
|
||||
// call native.Quote, dn is byte count it outputs
|
||||
ret := native.Quote(sp, nb, dp, &dn, 0)
|
||||
// update *buf length
|
||||
b.Len += dn
|
||||
|
||||
// no need more output
|
||||
if ret >= 0 {
|
||||
break
|
||||
}
|
||||
|
||||
// double buf size
|
||||
*b = rt.GrowSlice(typeByte, *b, b.Cap*2)
|
||||
// ret is the complement of consumed input
|
||||
ret = ^ret
|
||||
// update input buffer
|
||||
nb -= ret
|
||||
sp = unsafe.Pointer(uintptr(sp) + uintptr(ret))
|
||||
}
|
||||
|
||||
runtime.KeepAlive(buf)
|
||||
runtime.KeepAlive(sp)
|
||||
*buf = append(*buf, '"')
|
||||
}
|
||||
|
||||
func unquote(src string) (string, types.ParsingError) {
|
||||
return uq.String(src)
|
||||
*buf = alg.Quote(*buf, val, false)
|
||||
}
|
||||
|
||||
func (self *Parser) decodeValue() (val types.JsonState) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// +build !amd64,!arm64 go1.24 !go1.17 arm64,!go1.20
|
||||
// +build !amd64,!arm64 go1.26 !go1.17 arm64,!go1.20
|
||||
|
||||
/*
|
||||
* Copyright 2022 ByteDance Inc.
|
||||
@@ -23,28 +23,17 @@ import (
|
||||
`unicode/utf8`
|
||||
|
||||
`github.com/bytedance/sonic/internal/native/types`
|
||||
`github.com/bytedance/sonic/internal/rt`
|
||||
`github.com/bytedance/sonic/internal/compat`
|
||||
)
|
||||
|
||||
func init() {
|
||||
println("WARNING:(ast) sonic only supports go1.17~1.23, but your environment is not suitable")
|
||||
compat.Warn("sonic/ast")
|
||||
}
|
||||
|
||||
func quote(buf *[]byte, val string) {
|
||||
quoteString(buf, val)
|
||||
}
|
||||
|
||||
// unquote unescapes a internal JSON string (it doesn't count quotas at the begining and end)
|
||||
func unquote(src string) (string, types.ParsingError) {
|
||||
sp := rt.IndexChar(src, -1)
|
||||
out, ok := unquoteBytes(rt.BytesFrom(sp, len(src)+2, len(src)+2))
|
||||
if !ok {
|
||||
return "", types.ERR_INVALID_ESCAPE
|
||||
}
|
||||
return rt.Mem2Str(out), 0
|
||||
}
|
||||
|
||||
|
||||
func (self *Parser) decodeValue() (val types.JsonState) {
|
||||
e, v := decodeValue(self.s, self.p, self.dbuf == nil)
|
||||
if e < 0 {
|
||||
|
||||
@@ -25,10 +25,9 @@ import (
|
||||
"github.com/bytedance/sonic/internal/native/types"
|
||||
"github.com/bytedance/sonic/internal/rt"
|
||||
"github.com/bytedance/sonic/internal/utils"
|
||||
"github.com/bytedance/sonic/unquote"
|
||||
)
|
||||
|
||||
// Hack: this is used for both checking space and cause firendly compile errors in 32-bit arch.
|
||||
const _Sonic_Not_Support_32Bit_Arch__Checking_32Bit_Arch_Here = (1 << ' ') | (1 << '\t') | (1 << '\r') | (1 << '\n')
|
||||
|
||||
var bytesNull = []byte("null")
|
||||
|
||||
@@ -40,17 +39,13 @@ const (
|
||||
bytesArray = "[]"
|
||||
)
|
||||
|
||||
func isSpace(c byte) bool {
|
||||
return (int(1<<c) & _Sonic_Not_Support_32Bit_Arch__Checking_32Bit_Arch_Here) != 0
|
||||
}
|
||||
|
||||
//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 !isSpace(*(*byte)(unsafe.Pointer(sp))) {
|
||||
if !utils.IsSpace(*(*byte)(unsafe.Pointer(sp))) {
|
||||
break
|
||||
}
|
||||
sp += 1
|
||||
@@ -107,13 +102,13 @@ func decodeString(src string, pos int) (ret int, v string) {
|
||||
return ret, v
|
||||
}
|
||||
|
||||
vv, ok := unquoteBytes(rt.Str2Mem(src[pos:ret]))
|
||||
if !ok {
|
||||
result, err := unquote.String(src[pos:ret])
|
||||
if err != 0 {
|
||||
return -int(types.ERR_INVALID_CHAR), ""
|
||||
}
|
||||
|
||||
runtime.KeepAlive(src)
|
||||
return ret, rt.Mem2Str(vv)
|
||||
return ret, result
|
||||
}
|
||||
|
||||
func decodeBinary(src string, pos int) (ret int, v []byte) {
|
||||
@@ -549,7 +544,7 @@ func _DecodeString(src string, pos int, needEsc bool, validStr bool) (v string,
|
||||
return str, p.p, true
|
||||
}
|
||||
/* unquote the string */
|
||||
out, err := unquote(str)
|
||||
out, err := unquote.String(str)
|
||||
/* check for errors */
|
||||
if err != 0 {
|
||||
return "", -int(err), true
|
||||
|
||||
@@ -20,8 +20,9 @@ import (
|
||||
"sync"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/bytedance/gopkg/lang/dirtmake"
|
||||
"github.com/bytedance/sonic/internal/rt"
|
||||
"github.com/bytedance/sonic/option"
|
||||
"github.com/bytedance/sonic/option"
|
||||
)
|
||||
|
||||
func quoteString(e *[]byte, s string) {
|
||||
@@ -95,6 +96,11 @@ func (self *Node) MarshalJSON() ([]byte, error) {
|
||||
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 {
|
||||
@@ -105,7 +111,7 @@ func (self *Node) MarshalJSON() ([]byte, error) {
|
||||
if !rt.CanSizeResue(cap(*buf)) {
|
||||
ret = *buf
|
||||
} else {
|
||||
ret = make([]byte, len(*buf))
|
||||
ret = dirtmake.Bytes(len(*buf), len(*buf))
|
||||
copy(ret, *buf)
|
||||
freeBuffer(buf)
|
||||
}
|
||||
|
||||
@@ -173,10 +173,10 @@ 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.
|
||||
//
|
||||
// Especailly, if the node is not V_ARRAY or V_OBJECT,
|
||||
// Especially, if the node is not V_ARRAY or V_OBJECT,
|
||||
// the node itself will be returned and Sequence.Index == -1.
|
||||
//
|
||||
// NOTICE: A unsetted node WON'T trigger sc, but its index still counts into Path.Index
|
||||
// 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
|
||||
|
||||
@@ -64,14 +64,14 @@ type Node struct {
|
||||
// UnmarshalJSON is just an adapter to json.Unmarshaler.
|
||||
// If you want better performance, use Searcher.GetByPath() directly
|
||||
func (self *Node) UnmarshalJSON(data []byte) (err error) {
|
||||
*self = NewRaw(string(data))
|
||||
return self.Check()
|
||||
*self = newRawNode(rt.Mem2Str(data), switchRawType(data[0]), false)
|
||||
return nil
|
||||
}
|
||||
|
||||
/** Node Type Accessor **/
|
||||
|
||||
// Type returns json type represented by the node
|
||||
// It will be one of belows:
|
||||
// It will be one of bellows:
|
||||
// V_NONE = 0 (empty node, key not exists)
|
||||
// V_ERROR = 1 (error node)
|
||||
// V_NULL = 2 (json value `null`, key exists)
|
||||
@@ -89,7 +89,7 @@ func (self Node) Type() int {
|
||||
}
|
||||
|
||||
// Type concurrently-safe returns json type represented by the node
|
||||
// It will be one of belows:
|
||||
// It will be one of bellows:
|
||||
// V_NONE = 0 (empty node, key not exists)
|
||||
// V_ERROR = 1 (error node)
|
||||
// V_NULL = 2 (json value `null`, key exists)
|
||||
@@ -140,7 +140,7 @@ func (self *Node) Check() error {
|
||||
|
||||
// isRaw returns true if node's underlying value is raw json
|
||||
//
|
||||
// Deprecated: not concurent safe
|
||||
// Deprecated: not concurrent safe
|
||||
func (self Node) IsRaw() bool {
|
||||
return self.t & _V_RAW != 0
|
||||
}
|
||||
@@ -440,7 +440,7 @@ func (self *Node) String() (string, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// StrictString returns string value (unescaped), includeing V_STRING, V_ANY of string.
|
||||
// StrictString returns string value (unescaped), including V_STRING, V_ANY of string.
|
||||
// In other cases, it will return empty string.
|
||||
func (self *Node) StrictString() (string, error) {
|
||||
if err := self.checkRaw(); err != nil {
|
||||
@@ -509,7 +509,24 @@ func (self *Node) Float64() (float64, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// Float64 exports underlying float64 value, includeing V_NUMBER, V_ANY
|
||||
func (self *Node) StrictBool() (bool, error) {
|
||||
if err := self.checkRaw(); err!= nil {
|
||||
return false, err
|
||||
}
|
||||
switch self.t {
|
||||
case types.V_TRUE : return true, nil
|
||||
case types.V_FALSE : return false, nil
|
||||
case _V_ANY :
|
||||
any := self.packAny()
|
||||
switch v := any.(type) {
|
||||
case bool : return v, nil
|
||||
default : return false, ErrUnsupportType
|
||||
}
|
||||
default : return false, ErrUnsupportType
|
||||
}
|
||||
}
|
||||
|
||||
// Float64 exports underlying float64 value, including V_NUMBER, V_ANY
|
||||
func (self *Node) StrictFloat64() (float64, error) {
|
||||
if err := self.checkRaw(); err != nil {
|
||||
return 0.0, err
|
||||
@@ -527,7 +544,7 @@ func (self *Node) StrictFloat64() (float64, error) {
|
||||
}
|
||||
}
|
||||
|
||||
/** Sequencial Value Methods **/
|
||||
/** Sequential Value Methods **/
|
||||
|
||||
// Len returns children count of a array|object|string node
|
||||
// WARN: For partially loaded node, it also works but only counts the parsed children
|
||||
@@ -611,7 +628,7 @@ func (self *Node) Unset(key string) (bool, error) {
|
||||
if err := self.should(types.V_OBJECT); err != nil {
|
||||
return false, err
|
||||
}
|
||||
// NOTICE: must get acurate length before deduct
|
||||
// NOTICE: must get accurate length before deduct
|
||||
if err := self.skipAllKey(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
@@ -657,7 +674,7 @@ func (self *Node) SetAnyByIndex(index int, val interface{}) (bool, error) {
|
||||
return self.SetByIndex(index, NewAny(val))
|
||||
}
|
||||
|
||||
// UnsetByIndex REOMVE (softly) the node of given index.
|
||||
// UnsetByIndex REMOVE (softly) the node of given index.
|
||||
//
|
||||
// WARN: this will change address of elements, which is a dangerous action.
|
||||
// Use Unset() for object or Pop() for array instead.
|
||||
@@ -776,7 +793,7 @@ func (self *Node) Pop() error {
|
||||
}
|
||||
|
||||
// Move moves the child at src index to dst index,
|
||||
// meanwhile slides sliblings from src+1 to dst.
|
||||
// meanwhile slides siblings from src+1 to dst.
|
||||
//
|
||||
// WARN: this will change address of elements, which is a dangerous action.
|
||||
func (self *Node) Move(dst, src int) error {
|
||||
@@ -816,7 +833,7 @@ func (self *Node) Move(dst, src int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetAny wraps val with V_ANY node, and Add() the node.
|
||||
// AddAny wraps val with V_ANY node, and Add() the node.
|
||||
func (self *Node) AddAny(val interface{}) error {
|
||||
return self.Add(NewAny(val))
|
||||
}
|
||||
@@ -938,7 +955,7 @@ func (self *Node) Map() (map[string]interface{}, error) {
|
||||
return self.toGenericObject()
|
||||
}
|
||||
|
||||
// MapUseNumber loads all keys of an object node, with numeric nodes casted to json.Number
|
||||
// MapUseNumber loads all keys of an object node, with numeric nodes cast to json.Number
|
||||
func (self *Node) MapUseNumber() (map[string]interface{}, error) {
|
||||
if self.isAny() {
|
||||
any := self.packAny()
|
||||
@@ -957,7 +974,7 @@ func (self *Node) MapUseNumber() (map[string]interface{}, error) {
|
||||
return self.toGenericObjectUseNumber()
|
||||
}
|
||||
|
||||
// MapUseNode scans both parsed and non-parsed chidren nodes,
|
||||
// MapUseNode scans both parsed and non-parsed children nodes,
|
||||
// and map them by their keys
|
||||
func (self *Node) MapUseNode() (map[string]Node, error) {
|
||||
if self.isAny() {
|
||||
@@ -1083,7 +1100,7 @@ func (self *Node) Array() ([]interface{}, error) {
|
||||
return self.toGenericArray()
|
||||
}
|
||||
|
||||
// ArrayUseNumber loads all indexes of an array node, with numeric nodes casted to json.Number
|
||||
// ArrayUseNumber loads all indexes of an array node, with numeric nodes cast to json.Number
|
||||
func (self *Node) ArrayUseNumber() ([]interface{}, error) {
|
||||
if self.isAny() {
|
||||
any := self.packAny()
|
||||
@@ -1102,7 +1119,7 @@ func (self *Node) ArrayUseNumber() ([]interface{}, error) {
|
||||
return self.toGenericArrayUseNumber()
|
||||
}
|
||||
|
||||
// ArrayUseNode copys both parsed and non-parsed chidren nodes,
|
||||
// ArrayUseNode copies both parsed and non-parsed children nodes,
|
||||
// and indexes them by original order
|
||||
func (self *Node) ArrayUseNode() ([]Node, error) {
|
||||
if self.isAny() {
|
||||
@@ -1147,9 +1164,9 @@ func (self *Node) unsafeArray() (*linkedNodes, error) {
|
||||
return (*linkedNodes)(self.p), nil
|
||||
}
|
||||
|
||||
// Interface loads all children under all pathes from this node,
|
||||
// Interface loads all children under all paths from this node,
|
||||
// and converts itself as generic type.
|
||||
// WARN: all numberic nodes are casted to float64
|
||||
// WARN: all numeric nodes are cast to float64
|
||||
func (self *Node) Interface() (interface{}, error) {
|
||||
if err := self.checkRaw(); err != nil {
|
||||
return nil, err
|
||||
@@ -1193,7 +1210,7 @@ func (self *Node) packAny() interface{} {
|
||||
}
|
||||
|
||||
// InterfaceUseNumber works same with Interface()
|
||||
// except numberic nodes are casted to json.Number
|
||||
// except numeric nodes are cast to json.Number
|
||||
func (self *Node) InterfaceUseNumber() (interface{}, error) {
|
||||
if err := self.checkRaw(); err != nil {
|
||||
return nil, err
|
||||
@@ -1678,7 +1695,7 @@ func NewBytes(src []byte) Node {
|
||||
if len(src) == 0 {
|
||||
panic("empty src bytes")
|
||||
}
|
||||
out := rt.EncodeBase64(src)
|
||||
out := rt.EncodeBase64ToString(src)
|
||||
return NewString(out)
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,8 @@ import (
|
||||
|
||||
"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 (
|
||||
@@ -63,7 +65,7 @@ func (self *Parser) delim() types.ParsingError {
|
||||
return types.ERR_EOF
|
||||
}
|
||||
|
||||
/* check for the delimtier */
|
||||
/* check for the delimiter */
|
||||
if self.s[p] != ':' {
|
||||
return types.ERR_INVALID_CHAR
|
||||
}
|
||||
@@ -82,7 +84,7 @@ func (self *Parser) object() types.ParsingError {
|
||||
return types.ERR_EOF
|
||||
}
|
||||
|
||||
/* check for the delimtier */
|
||||
/* check for the delimiter */
|
||||
if self.s[p] != '{' {
|
||||
return types.ERR_INVALID_CHAR
|
||||
}
|
||||
@@ -101,7 +103,7 @@ func (self *Parser) array() types.ParsingError {
|
||||
return types.ERR_EOF
|
||||
}
|
||||
|
||||
/* check for the delimtier */
|
||||
/* check for the delimiter */
|
||||
if self.s[p] != '[' {
|
||||
return types.ERR_INVALID_CHAR
|
||||
}
|
||||
@@ -113,13 +115,13 @@ func (self *Parser) array() types.ParsingError {
|
||||
|
||||
func (self *Parser) lspace(sp int) int {
|
||||
ns := len(self.s)
|
||||
for ; sp<ns && isSpace(self.s[sp]); sp+=1 {}
|
||||
for ; sp<ns && utils.IsSpace(self.s[sp]); sp+=1 {}
|
||||
|
||||
return sp
|
||||
}
|
||||
|
||||
func (self *Parser) backward() {
|
||||
for ; self.p >= 0 && isSpace(self.s[self.p]); self.p-=1 {}
|
||||
for ; self.p >= 0 && utils.IsSpace(self.s[self.p]); self.p-=1 {}
|
||||
}
|
||||
|
||||
func (self *Parser) decodeArray(ret *linkedNodes) (Node, types.ParsingError) {
|
||||
@@ -217,7 +219,7 @@ func (self *Parser) decodeObject(ret *linkedPairs) (Node, types.ParsingError) {
|
||||
|
||||
/* check for escape sequence */
|
||||
if njs.Ep != -1 {
|
||||
if key, err = unquote(key); err != 0 {
|
||||
if key, err = unquote.String(key); err != 0 {
|
||||
return Node{}, err
|
||||
}
|
||||
}
|
||||
@@ -282,7 +284,7 @@ func (self *Parser) decodeString(iv int64, ep int) (Node, types.ParsingError) {
|
||||
}
|
||||
|
||||
/* unquote the string */
|
||||
out, err := unquote(s)
|
||||
out, err := unquote.String(s)
|
||||
|
||||
/* check for errors */
|
||||
if err != 0 {
|
||||
@@ -392,7 +394,7 @@ func (self *Parser) searchKey(match string) types.ParsingError {
|
||||
|
||||
/* check for escape sequence */
|
||||
if njs.Ep != -1 {
|
||||
if key, err = unquote(key); err != 0 {
|
||||
if key, err = unquote.String(key); err != 0 {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -573,7 +575,7 @@ func (self *Node) skipNextPair() (*Pair) {
|
||||
|
||||
/* check for escape sequence */
|
||||
if njs.Ep != -1 {
|
||||
if key, err = unquote(key); err != 0 {
|
||||
if key, err = unquote.String(key); err != 0 {
|
||||
return newErrorPair(parser.syntaxError(err))
|
||||
}
|
||||
}
|
||||
@@ -638,7 +640,7 @@ func Loads(src string) (int, interface{}, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// LoadsUseNumber parse all json into interface{}, with numeric nodes casted to json.Number
|
||||
// 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()
|
||||
@@ -692,7 +694,7 @@ func (self *Parser) ExportError(err types.ParsingError) error {
|
||||
}
|
||||
|
||||
func backward(src string, i int) int {
|
||||
for ; i>=0 && isSpace(src[i]); i-- {}
|
||||
for ; i>=0 && utils.IsSpace(src[i]); i-- {}
|
||||
return i
|
||||
}
|
||||
|
||||
|
||||
@@ -17,126 +17,12 @@
|
||||
package ast
|
||||
|
||||
import (
|
||||
"unicode/utf8"
|
||||
"unsafe"
|
||||
|
||||
"github.com/bytedance/sonic/internal/rt"
|
||||
)
|
||||
|
||||
//go:noescape
|
||||
//go:linkname memmove runtime.memmove
|
||||
//goland:noinspection GoUnusedParameter
|
||||
func memmove(to unsafe.Pointer, from unsafe.Pointer, n uintptr)
|
||||
|
||||
//go:linkname unsafe_NewArray reflect.unsafe_NewArray
|
||||
//goland:noinspection GoUnusedParameter
|
||||
func unsafe_NewArray(typ *rt.GoType, n int) unsafe.Pointer
|
||||
|
||||
//go:nosplit
|
||||
func mem2ptr(s []byte) unsafe.Pointer {
|
||||
return (*rt.GoSlice)(unsafe.Pointer(&s)).Ptr
|
||||
}
|
||||
|
||||
var safeSet = [utf8.RuneSelf]bool{
|
||||
' ': true,
|
||||
'!': true,
|
||||
'"': false,
|
||||
'#': true,
|
||||
'$': true,
|
||||
'%': true,
|
||||
'&': true,
|
||||
'\'': true,
|
||||
'(': true,
|
||||
')': true,
|
||||
'*': true,
|
||||
'+': true,
|
||||
',': true,
|
||||
'-': true,
|
||||
'.': true,
|
||||
'/': true,
|
||||
'0': true,
|
||||
'1': true,
|
||||
'2': true,
|
||||
'3': true,
|
||||
'4': true,
|
||||
'5': true,
|
||||
'6': true,
|
||||
'7': true,
|
||||
'8': true,
|
||||
'9': true,
|
||||
':': true,
|
||||
';': true,
|
||||
'<': true,
|
||||
'=': true,
|
||||
'>': true,
|
||||
'?': true,
|
||||
'@': true,
|
||||
'A': true,
|
||||
'B': true,
|
||||
'C': true,
|
||||
'D': true,
|
||||
'E': true,
|
||||
'F': true,
|
||||
'G': true,
|
||||
'H': true,
|
||||
'I': true,
|
||||
'J': true,
|
||||
'K': true,
|
||||
'L': true,
|
||||
'M': true,
|
||||
'N': true,
|
||||
'O': true,
|
||||
'P': true,
|
||||
'Q': true,
|
||||
'R': true,
|
||||
'S': true,
|
||||
'T': true,
|
||||
'U': true,
|
||||
'V': true,
|
||||
'W': true,
|
||||
'X': true,
|
||||
'Y': true,
|
||||
'Z': true,
|
||||
'[': true,
|
||||
'\\': false,
|
||||
']': true,
|
||||
'^': true,
|
||||
'_': true,
|
||||
'`': true,
|
||||
'a': true,
|
||||
'b': true,
|
||||
'c': true,
|
||||
'd': true,
|
||||
'e': true,
|
||||
'f': true,
|
||||
'g': true,
|
||||
'h': true,
|
||||
'i': true,
|
||||
'j': true,
|
||||
'k': true,
|
||||
'l': true,
|
||||
'm': true,
|
||||
'n': true,
|
||||
'o': true,
|
||||
'p': true,
|
||||
'q': true,
|
||||
'r': true,
|
||||
's': true,
|
||||
't': true,
|
||||
'u': true,
|
||||
'v': true,
|
||||
'w': true,
|
||||
'x': true,
|
||||
'y': true,
|
||||
'z': true,
|
||||
'{': true,
|
||||
'|': true,
|
||||
'}': true,
|
||||
'~': true,
|
||||
'\u007f': true,
|
||||
}
|
||||
|
||||
var hex = "0123456789abcdef"
|
||||
|
||||
//go:linkname unquoteBytes encoding/json.unquoteBytes
|
||||
func unquoteBytes(s []byte) (t []byte, ok bool)
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
`errors`
|
||||
|
||||
`github.com/bytedance/sonic/internal/native/types`
|
||||
`github.com/bytedance/sonic/unquote`
|
||||
)
|
||||
|
||||
// Visitor handles the callbacks during preorder traversal of a JSON AST.
|
||||
@@ -178,7 +179,7 @@ func (self *traverser) decodeArray() error {
|
||||
/* 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 entiry object
|
||||
// NOTICE: for user needs to skip entry object
|
||||
self.parser.p -= 1
|
||||
if _, e := self.parser.skipFast(); e != 0 {
|
||||
return e
|
||||
@@ -233,7 +234,7 @@ func (self *traverser) decodeObject() error {
|
||||
/* 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 entiry object
|
||||
// NOTICE: for user needs to skip entry object
|
||||
self.parser.p -= 1
|
||||
if _, e := self.parser.skipFast(); e != 0 {
|
||||
return e
|
||||
@@ -270,7 +271,7 @@ func (self *traverser) decodeObject() error {
|
||||
|
||||
/* check for escape sequence */
|
||||
if njs.Ep != -1 {
|
||||
if key, err = unquote(key); err != 0 {
|
||||
if key, err = unquote.String(key); err != 0 {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -320,7 +321,7 @@ func (self *traverser) decodeString(iv int64, ep int) error {
|
||||
}
|
||||
|
||||
/* unquote the string */
|
||||
out, err := unquote(s)
|
||||
out, err := unquote.String(s)
|
||||
if err != 0 {
|
||||
return err
|
||||
}
|
||||
@@ -328,5 +329,5 @@ func (self *traverser) decodeString(iv int64, ep int) error {
|
||||
}
|
||||
|
||||
// If visitor return this error on `OnObjectBegin()` or `OnArrayBegin()`,
|
||||
// the transverer will skip entiry object or array
|
||||
// the traverser will skip entry object or array
|
||||
var VisitOPSkip = errors.New("")
|
||||
@@ -1,4 +1,4 @@
|
||||
// +build !amd64,!arm64 go1.24 !go1.17 arm64,!go1.20
|
||||
// +build !amd64,!arm64 go1.26 !go1.17 arm64,!go1.20
|
||||
|
||||
/*
|
||||
* Copyright 2021 ByteDance Inc.
|
||||
@@ -87,7 +87,17 @@ func (cfg frozenConfig) UnmarshalFromString(buf string, val interface{}) error {
|
||||
if cfg.DisallowUnknownFields {
|
||||
dec.DisallowUnknownFields()
|
||||
}
|
||||
return dec.Decode(val)
|
||||
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
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// +build !amd64,!arm64 go1.24 !go1.17 arm64,!go1.20
|
||||
//go:build (!amd64 && !arm64) || go1.26 || !go1.17 || (arm64 && !go1.20)
|
||||
// +build !amd64,!arm64 go1.26 !go1.17 arm64,!go1.20
|
||||
|
||||
/*
|
||||
* Copyright 2023 ByteDance Inc.
|
||||
@@ -19,30 +20,33 @@
|
||||
package decoder
|
||||
|
||||
import (
|
||||
`bytes`
|
||||
`encoding/json`
|
||||
`io`
|
||||
`reflect`
|
||||
`unsafe`
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"reflect"
|
||||
"unsafe"
|
||||
|
||||
`github.com/bytedance/sonic/internal/native/types`
|
||||
`github.com/bytedance/sonic/option`
|
||||
"github.com/bytedance/sonic/internal/decoder/consts"
|
||||
"github.com/bytedance/sonic/internal/native/types"
|
||||
"github.com/bytedance/sonic/option"
|
||||
"github.com/bytedance/sonic/internal/compat"
|
||||
)
|
||||
|
||||
func init() {
|
||||
println("WARNING: sonic/decoder only supports (Go1.17~1.23 && CPU amd64) or (go1.20~1.23 && CPU arm64), but your environment is not suitable")
|
||||
compat.Warn("sonic/decoder")
|
||||
}
|
||||
|
||||
const (
|
||||
_F_use_int64 = 0
|
||||
_F_disable_urc = 2
|
||||
_F_disable_unknown = 3
|
||||
_F_copy_string = 4
|
||||
_F_use_int64 = consts.F_use_int64
|
||||
_F_disable_urc = consts.F_disable_unknown
|
||||
_F_disable_unknown = consts.F_disable_unknown
|
||||
_F_copy_string = consts.F_copy_string
|
||||
|
||||
_F_use_number = types.B_USE_NUMBER
|
||||
_F_validate_string = types.B_VALIDATE_STRING
|
||||
_F_allow_control = types.B_ALLOW_CONTROL
|
||||
_F_no_validate_json = types.B_NO_VALIDATE_JSON
|
||||
_F_use_number = consts.F_use_number
|
||||
_F_validate_string = consts.F_validate_string
|
||||
_F_allow_control = consts.F_allow_control
|
||||
_F_no_validate_json = consts.F_no_validate_json
|
||||
_F_case_sensitive = consts.F_case_sensitive
|
||||
)
|
||||
|
||||
type Options uint64
|
||||
@@ -55,6 +59,7 @@ const (
|
||||
OptionCopyString Options = 1 << _F_copy_string
|
||||
OptionValidateString Options = 1 << _F_validate_string
|
||||
OptionNoValidateJSON Options = 1 << _F_no_validate_json
|
||||
OptionCaseSensitive Options = 1 << _F_case_sensitive
|
||||
)
|
||||
|
||||
func (self *Decoder) SetOptions(opts Options) {
|
||||
@@ -192,5 +197,5 @@ func (s SyntaxError) Error() string {
|
||||
return (*json.SyntaxError)(unsafe.Pointer(&s)).Error()
|
||||
}
|
||||
|
||||
// MismatchTypeError represents dismatching between json and object
|
||||
type MismatchTypeError json.UnmarshalTypeError
|
||||
// MismatchTypeError represents mismatching between json and object
|
||||
type MismatchTypeError json.UnmarshalTypeError
|
||||
@@ -1,5 +1,5 @@
|
||||
//go:build (amd64 && go1.17 && !go1.24) || (arm64 && go1.20 && !go1.24)
|
||||
// +build amd64,go1.17,!go1.24 arm64,go1.20,!go1.24
|
||||
//go:build (amd64 && go1.17 && !go1.26) || (arm64 && go1.20 && !go1.26)
|
||||
// +build amd64,go1.17,!go1.26 arm64,go1.20,!go1.26
|
||||
|
||||
|
||||
/*
|
||||
@@ -30,7 +30,7 @@ type Decoder = api.Decoder
|
||||
// SyntaxError represents json syntax error
|
||||
type SyntaxError = api.SyntaxError
|
||||
|
||||
// MismatchTypeError represents dismatching between json and object
|
||||
// MismatchTypeError represents mismatching between json and object
|
||||
type MismatchTypeError = api.MismatchTypeError
|
||||
|
||||
// Options for decode.
|
||||
@@ -44,6 +44,7 @@ const (
|
||||
OptionCopyString Options = api.OptionCopyString
|
||||
OptionValidateString Options = api.OptionValidateString
|
||||
OptionNoValidateJSON Options = api.OptionNoValidateJSON
|
||||
OptionCaseSensitive Options = api.OptionCaseSensitive
|
||||
)
|
||||
|
||||
// StreamDecoder is the decoder context object for streaming input.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// +build !amd64,!arm64 go1.24 !go1.17 arm64,!go1.20
|
||||
// +build !amd64,!arm64 go1.26 !go1.17 arm64,!go1.20
|
||||
|
||||
/*
|
||||
* Copyright 2023 ByteDance Inc.
|
||||
@@ -19,16 +19,17 @@
|
||||
package encoder
|
||||
|
||||
import (
|
||||
`io`
|
||||
`io`
|
||||
`bytes`
|
||||
`encoding/json`
|
||||
`reflect`
|
||||
|
||||
`github.com/bytedance/sonic/option`
|
||||
`github.com/bytedance/sonic/internal/compat`
|
||||
)
|
||||
|
||||
func init() {
|
||||
println("WARNING:(encoder) sonic only supports (Go1.17~1.23 && CPU amd64) or (G01.20~1.23 && CPU arm64) , but your environment is not suitable")
|
||||
compat.Warn("sonic/encoder")
|
||||
}
|
||||
|
||||
// EnableFallback indicates if encoder use fallback
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// +build amd64,go1.17,!go1.24 arm64,go1.20,!go1.24
|
||||
// +build amd64,go1.17,!go1.26 arm64,go1.20,!go1.26
|
||||
|
||||
/*
|
||||
* Copyright 2023 ByteDance Inc.
|
||||
@@ -71,7 +71,8 @@ const (
|
||||
// CompatibleWithStd is used to be compatible with std encoder.
|
||||
CompatibleWithStd Options = encoder.CompatibleWithStd
|
||||
|
||||
// Encode Infinity or Nan float into `null`, instead of returning an error.
|
||||
// EncodeNullForInfOrNan encodes Infinity or NaN float values as 'null'
|
||||
// instead of returning an error.
|
||||
EncodeNullForInfOrNan Options = encoder.EncodeNullForInfOrNan
|
||||
)
|
||||
|
||||
@@ -80,12 +81,12 @@ var (
|
||||
// Encode returns the JSON encoding of val, encoded with opts.
|
||||
Encode = encoder.Encode
|
||||
|
||||
// EncodeInto is like Encode but uses a user-supplied buffer instead of allocating a new one.
|
||||
EncodeIndented = encoder.EncodeIndented
|
||||
|
||||
// EncodeIndented is like Encode 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.
|
||||
EncodeIndented = encoder.EncodeIndented
|
||||
|
||||
// EncodeInto is like Encode but uses a user-supplied buffer instead of allocating a new one.
|
||||
EncodeInto = encoder.EncodeInto
|
||||
|
||||
// HTMLEscape appends to dst the JSON-encoded src with <, >, &, U+2028 and U+2029
|
||||
|
||||
@@ -5,5 +5,6 @@ use (
|
||||
./external_jsonlib_test
|
||||
./fuzz
|
||||
./generic_test
|
||||
./issue_test
|
||||
./loader
|
||||
)
|
||||
@@ -1 +1,11 @@
|
||||
github.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/bytedance/sonic/loader v0.2.3/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
|
||||
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
|
||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
|
||||
@@ -1,46 +0,0 @@
|
||||
// +build amd64,go1.17,!go1.24
|
||||
|
||||
/**
|
||||
* 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 base64
|
||||
|
||||
import (
|
||||
"github.com/cloudwego/base64x"
|
||||
)
|
||||
|
||||
func DecodeBase64(src string) ([]byte, error) {
|
||||
return base64x.StdEncoding.DecodeString(src)
|
||||
}
|
||||
|
||||
func EncodeBase64(buf []byte, src []byte) []byte {
|
||||
if len(src) == 0 {
|
||||
return append(buf, '"', '"')
|
||||
}
|
||||
buf = append(buf, '"')
|
||||
need := base64x.StdEncoding.EncodedLen(len(src))
|
||||
if cap(buf) - len(buf) < need {
|
||||
tmp := make([]byte, len(buf), len(buf) + need*2)
|
||||
copy(tmp, buf)
|
||||
buf = tmp
|
||||
}
|
||||
base64x.StdEncoding.Encode(buf[len(buf):cap(buf)], src)
|
||||
buf = buf[:len(buf) + need]
|
||||
buf = append(buf, '"')
|
||||
return buf
|
||||
}
|
||||
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
// +build !amd64 !go1.17 go1.24
|
||||
|
||||
/*
|
||||
* 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 base64
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
)
|
||||
|
||||
func EncodeBase64(buf []byte, src []byte) []byte {
|
||||
if len(src) == 0 {
|
||||
return append(buf, '"', '"')
|
||||
}
|
||||
buf = append(buf, '"')
|
||||
need := base64.StdEncoding.EncodedLen(len(src))
|
||||
if cap(buf) - len(buf) < need {
|
||||
tmp := make([]byte, len(buf), len(buf) + need*2)
|
||||
copy(tmp, buf)
|
||||
buf = tmp
|
||||
}
|
||||
base64.StdEncoding.Encode(buf[len(buf):cap(buf)], src)
|
||||
buf = buf[:len(buf) + need]
|
||||
buf = append(buf, '"')
|
||||
return buf
|
||||
}
|
||||
|
||||
func DecodeBase64(src string) ([]byte, error) {
|
||||
return base64.StdEncoding.DecodeString(src)
|
||||
}
|
||||
@@ -23,16 +23,12 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
V_strhash = rt.UnpackEface(strhash)
|
||||
V_strhash = rt.UnpackEface(rt.Strhash)
|
||||
S_strhash = *(*uintptr)(V_strhash.Value)
|
||||
)
|
||||
|
||||
//go:noescape
|
||||
//go:linkname strhash runtime.strhash
|
||||
func strhash(_ unsafe.Pointer, _ uintptr) uintptr
|
||||
|
||||
func StrHash(s string) uint64 {
|
||||
if v := strhash(unsafe.Pointer(&s), 0); v == 0 {
|
||||
if v := rt.Strhash(unsafe.Pointer(&s), 0); v == 0 {
|
||||
return 1
|
||||
} else {
|
||||
return uint64(v)
|
||||
|
||||