vault backup: 2025-04-11 14:31:16

This commit is contained in:
SeedList
2025-04-11 14:31:16 +08:00
parent 3b34a53929
commit eb4274e3e8
89 changed files with 0 additions and 24824 deletions

View File

@ -1,37 +0,0 @@
# vscode
.vscode
# Intellij
*.iml
.idea
# npm
node_modules
# Don't include the compiled main.js file in the repo.
# They should be uploaded to GitHub releases instead.
main.js
# Exclude sourcemaps
*.map
# obsidian
data.json
# Exclude macOS Finder (System Explorer) View States
.DS_Store
webpage-html-export.zip
app-styles.css
webpage-html-export.code-workspace
*.bat
build
dist
todo.txt
Dockerfile
vault/
output/

View File

@ -1,10 +0,0 @@
# top-most EditorConfig file
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = tab
indent_size = 4
tab_width = 4

View File

@ -1,2 +0,0 @@
npm node_modules
build

View File

@ -1,23 +0,0 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"env": { "node": true },
"plugins": [
"@typescript-eslint"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
],
"parserOptions": {
"sourceType": "module"
},
"rules": {
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": ["error", { "args": "none" }],
"@typescript-eslint/ban-ts-comment": "off",
"no-prototype-builtins": "off",
"@typescript-eslint/no-empty-function": "off"
}
}

View File

@ -1,13 +0,0 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
custom: ['https://www.buymeacoffee.com/nathangeorge', 'https://www.paypal.com/donate/?business=HHQBAXQQXT84Q&no_recurring=0&item_name=Hey+%F0%9F%91%8B+I+am+a+Computer+Science+student+working+on+Obsidian+plugins.+Thanks+for+your+support%21&currency_code=USD']

View File

@ -1,74 +0,0 @@
name: Bug Report
description: File a bug report
title: "[Bug]: "
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
- type: textarea
id: what-happened
attributes:
label: What happened?
description: Also what did you expect to happen?
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: Reproduction
description: Give detailed instructions to reproduce this problem
validations:
required: true
- type: input
id: last-working
attributes:
label: Last Working Version
description: Did this work in a previous version of the plugin? If so which one?
- type: input
id: version
attributes:
label: Version
description: What version of the plugin are you using?
validations:
required: true
- type: input
id: os
attributes:
label: Operating System
validations:
required: true
- type: input
id: ob-version
attributes:
label: Obsidian Version
description: What **current version** and **installer version** is shown in obsidian\'s general settings.
validations:
required: true
- type: dropdown
id: browsers
attributes:
label: Which browsers can you reproduce this in?
description: (optional)
multiple: true
options:
- Firefox
- Chrome
- Chrome IOS
- Safari
- Safari IOS
- Microsoft Edge
- type: textarea
id: logs
attributes:
label: Relevant log output
description: Please click the debug info button in the plugin settings and paste here. (If this is not relevant put N/A)
render: yaml
validations:
required: true
- type: textarea
id: additional
attributes:
label: Additional Info
description: If relevant please include a screenshot of the developer log `Ctrl + Shift + i`

View File

@ -1,5 +0,0 @@
blank_issues_enabled=false
contact_links:
- name: GitHub Disscussions
url: https://github.com/KosmosisDire/obsidian-webpage-export/discussions
about: If you have a question or need help please use create a discussion. Only create an issue for bugs or feature requests.

View File

@ -1,20 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@ -1,100 +0,0 @@
name: Autoreply to Bug Issues
on:
issues:
types: [opened]
jobs:
autoreply-to-bugs:
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- name: Check and Respond to Bug Issues
uses: actions/github-script@v6
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
const issueBody = context.payload.issue.body;
const issueTitle = context.payload.issue.title;
// Check if it's a bug issue
if (!issueTitle.toLowerCase().includes('[bug]')) {
console.log('Not a bug issue. Skipping.');
return;
}
console.log('Processing bug issue:', issueTitle);
// Extract version from the issue body
const versionMatch = issueBody ? issueBody.match(/### Version\s*([0-9.b-]+)/) : null;
if (!versionMatch) {
console.log('Version not specified in the issue. Skipping response.');
return;
}
const reportedVersion = versionMatch[1];
console.log('Reported version:', reportedVersion);
// Fetch the latest beta release
const releases = await github.rest.repos.listReleases({
owner: context.repo.owner,
repo: context.repo.repo
});
const latestBeta = releases.data.find(release => release.prerelease);
if (!latestBeta) {
console.log('No beta release found. Skipping response.');
return;
}
const latestBetaVersion = latestBeta.tag_name.replace(/^v/, '');
console.log('Latest beta version:', latestBetaVersion);
// Compare versions
if (compareVersions(reportedVersion, latestBetaVersion) >= 0) {
console.log('Reported version is latest or newer. No response needed.');
return;
}
// Respond to the issue
const response = `Thank you for reporting this issue. It is possible this issue has already been solved in the latest beta version.\nPlease try updating to the latest beta version of the plugin (${latestBetaVersion}) and see if the issue persists.\n\nYou can find the latest beta release here: ${latestBeta.html_url}.\nInstructions for installing are located in the readme.\n\nIf the problem continues after updating, please let us know, and we'll investigate further.`;
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: response
});
console.log('Response posted successfully.');
// Updated version comparison function
function compareVersions(v1, v2) {
const parts1 = v1.split(/[.-]/).map(part => isNaN(part) ? part : parseInt(part));
const parts2 = v2.split(/[.-]/).map(part => isNaN(part) ? part : parseInt(part));
const isV1Beta = parts1.some(part => typeof part === 'string' && part.toLowerCase().includes('b'));
const isV2Beta = parts2.some(part => typeof part === 'string' && part.toLowerCase().includes('b'));
// If one is beta and the other is not, the non-beta version is newer
if (isV1Beta && !isV2Beta) return -1;
if (!isV1Beta && isV2Beta) return 1;
for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
const part1 = parts1[i] || 0;
const part2 = parts2[i] || 0;
if (part1 === part2) continue;
if (typeof part1 === 'string' && typeof part2 === 'string') {
return part1.localeCompare(part2);
} else if (typeof part1 === 'string') {
return 1; // Consider string (beta) as newer within same version number
} else if (typeof part2 === 'string') {
return -1; // Consider string (beta) as newer within same version number
} else {
return part1 < part2 ? -1 : 1;
}
}
return 0;
}

View File

@ -1,118 +0,0 @@
name: Release Plugin Beta
on:
push:
tags:
- "*b"
env:
PLUGIN_NAME: webpage-html-export # Change this to match the id of your plugin.
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Use Node.js
uses: actions/setup-node@v1
with:
node-version: "20.x"
- name: Build
id: build
run: |
npm install
npm run build
mkdir ${{ env.PLUGIN_NAME }}
cp main.js manifest.json styles.css ${{ env.PLUGIN_NAME }}
zip -r ${{ env.PLUGIN_NAME }}.zip ${{ env.PLUGIN_NAME }}
ls
echo "::set-output name=tag_name::$(git tag --sort version:refname | tail -n 1)"
- name: Create Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ github.ref }}
with:
tag_name: ${{ github.ref }}
release_name: ${{ github.ref }}
draft: false
prerelease: true
- name: Upload zip file
id: upload-zip
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./${{ env.PLUGIN_NAME }}.zip
asset_name: ${{ env.PLUGIN_NAME }}-${{ steps.build.outputs.tag_name }}.zip
asset_content_type: application/zip
- name: Upload main.js
id: upload-main
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./main.js
asset_name: main.js
asset_content_type: text/javascript
- name: Upload manifest.json
id: upload-manifest
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./manifest.json
asset_name: manifest.json
asset_content_type: application/json
- name: Upload styles.css
id: upload-css
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./styles.css
asset_name: styles.css
asset_content_type: text/css
docker-image:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Get the version
id: get_version
run: echo ::set-output name=tag_name::$(git tag --sort version:refname | tail -n 1)
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
with:
platforms: linux/arm64,linux/amd64
push: true
tags: KosmosisDire/obsidian-webpage-export:beta , KosmosisDire/obsidian-webpage-export:${{ steps.get_version.outputs.tag_name }}
cache-from: type=gha
cache-to: type=gha,mode=max

View File

@ -1,82 +0,0 @@
name: Label Issues with Links
on:
issues:
types: [opened, edited]
jobs:
label-issues-with-links:
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- name: Check for Links and Label Issue
uses: actions/github-script@v6
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
const issueBody = context.payload.issue.body;
const issueNumber = context.issue.number;
console.log(`Processing issue #${issueNumber}`);
// Regular expression to match URLs
const urlRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/gi;
if (issueBody && urlRegex.test(issueBody)) {
console.log('Link detected in the issue body');
try {
await github.rest.issues.addLabels({
issue_number: issueNumber,
owner: context.repo.owner,
repo: context.repo.repo,
labels: ['has-link']
});
console.log('Added "has-link" label to the issue');
} catch (error) {
if (error.status === 404) {
console.log('Label "has-link" does not exist. Creating it...');
await github.rest.issues.createLabel({
owner: context.repo.owner,
repo: context.repo.repo,
name: 'has-link',
color: '0366d6' // You can change this color code as needed
});
await github.rest.issues.addLabels({
issue_number: issueNumber,
owner: context.repo.owner,
repo: context.repo.repo,
labels: ['has-link']
});
console.log('Created "has-link" label and added it to the issue');
} else {
console.error('Error adding label:', error);
}
}
} else {
console.log('No link detected in the issue body');
// Check if the label exists and remove it if it does
try {
const labels = await github.rest.issues.listLabelsOnIssue({
issue_number: issueNumber,
owner: context.repo.owner,
repo: context.repo.repo
});
if (labels.data.some(label => label.name === 'has-link')) {
await github.rest.issues.removeLabel({
issue_number: issueNumber,
owner: context.repo.owner,
repo: context.repo.repo,
name: 'has-link'
});
console.log('Removed "has-link" label from the issue');
}
} catch (error) {
console.error('Error checking or removing label:', error);
}
}

View File

@ -1,83 +0,0 @@
name: Update All Existing Issue Titles
on:
workflow_dispatch:
jobs:
update-issue-titles:
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- name: Update All Issue Titles
uses: actions/github-script@v6
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
const perPage = 100;
let page = 1;
let allIssues = [];
// Fetch all issues
while (true) {
const issues = await github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'all',
per_page: perPage,
page: page
});
allIssues = allIssues.concat(issues.data);
if (issues.data.length < perPage) break;
page++;
}
console.log(`Total issues found: ${allIssues.length}`);
for (const issue of allIssues) {
const currentTitle = issue.title;
// Check if the title starts with [Bug] and doesn't already have a version
if (currentTitle.trim().toLowerCase().startsWith('[bug]') &&
!currentTitle.match(/^\[Bug\]\s*\[[^\]]+\]/i)) {
console.log(`Processing issue #${issue.number}: "${currentTitle}"`);
// Extract version from the issue body, handling null case
let version = 'unknown';
if (issue.body) {
const versionMatch = issue.body.match(/### Version\s*([0-9.b-]+)/);
if (versionMatch) {
version = versionMatch[1];
}
} else {
console.log(`Issue #${issue.number} has no body`);
}
// Remove any existing version tag if present and add new tags
let newTitle = currentTitle.replace(/^\[Bug\]\s*/i, '').trim();
newTitle = `[Bug] [${version}]${newTitle}`;
console.log(`New title: "${newTitle}"`);
try {
await github.rest.issues.update({
issue_number: issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
title: newTitle
});
console.log(`Successfully updated issue #${issue.number}`);
} catch (error) {
console.error(`Failed to update issue #${issue.number}`);
console.error(error);
}
} else {
console.log(`Skipping issue #${issue.number}: "${currentTitle}" (already formatted or not a bug)`);
}
}
console.log('Finished processing all issues');

View File

@ -1,57 +0,0 @@
name: Update Issue Title with Version
on:
issues:
types: [opened]
jobs:
update-issue-title:
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- name: Update Issue Title
uses: actions/github-script@v6
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
const issueNumber = context.issue.number;
const issueBody = context.payload.issue.body;
console.log('Extracting version from issue body...');
// Extract version from the issue body
const versionMatch = issueBody.match(/### Version\s*([0-9.b-]+)/);
const version = versionMatch ? versionMatch[1] : 'unknown';
console.log(`Extracted version: ${version}`);
const currentTitle = context.payload.issue.title;
console.log(`Current title: "${currentTitle}"`);
// Check if the title starts with [Bug]
if (currentTitle.trim().toLowerCase().startsWith('[bug]')) {
// Remove any existing version tag if present
let newTitle = currentTitle.replace(/^\[Bug\]\s*(\[[^\]]+\]\s*)?/i, '').trim();
// Add [Bug] and [version] tags with a colon
newTitle = `[Bug] [${version}]${newTitle}`;
console.log(`New title: "${newTitle}"`);
try {
await github.rest.issues.update({
issue_number: issueNumber,
owner: context.repo.owner,
repo: context.repo.repo,
title: newTitle
});
console.log('Successfully updated issue title');
} catch (error) {
console.error('Failed to update issue title');
console.error(error);
core.setFailed('Failed to update issue title');
}
} else {
console.log('Title does not start with [Bug]. No changes made.');
}

View File

@ -1,119 +0,0 @@
name: Release Obsidian plugin
on:
push:
tags:
- "*"
- "!*b"
env:
PLUGIN_NAME: webpage-html-export # Change this to match the id of your plugin.
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Use Node.js
uses: actions/setup-node@v1
with:
node-version: "14.x"
- name: Build
id: build
run: |
npm install
npm run build
mkdir ${{ env.PLUGIN_NAME }}
cp main.js manifest.json styles.css ${{ env.PLUGIN_NAME }}
zip -r ${{ env.PLUGIN_NAME }}.zip ${{ env.PLUGIN_NAME }}
ls
echo "::set-output name=tag_name::$(git tag --sort version:refname | tail -n 1)"
- name: Create Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ github.ref }}
with:
tag_name: ${{ github.ref }}
release_name: ${{ github.ref }}
draft: false
prerelease: false
- name: Upload zip file
id: upload-zip
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./${{ env.PLUGIN_NAME }}.zip
asset_name: ${{ env.PLUGIN_NAME }}-${{ steps.build.outputs.tag_name }}.zip
asset_content_type: application/zip
- name: Upload main.js
id: upload-main
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./main.js
asset_name: main.js
asset_content_type: text/javascript
- name: Upload manifest.json
id: upload-manifest
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./manifest.json
asset_name: manifest.json
asset_content_type: application/json
- name: Upload styles.css
id: upload-css
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./styles.css
asset_name: styles.css
asset_content_type: text/css
docker-image:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Get the version
id: get_version
run: echo ::set-output name=tag_name::$(git tag --sort version:refname | tail -n 1)
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
with:
platforms: linux/arm64,linux/amd64
push: true
tags: KosmosisDire/obsidian-webpage-export:latest , KosmosisDire/obsidian-webpage-export:${{ steps.get_version.outputs.tag_name }}
cache-from: type=gha
cache-to: type=gha,mode=max

View File

@ -1,36 +0,0 @@
# vscode
.vscode
# Intellij
*.iml
.idea
# npm
node_modules
# Don't include the compiled main.js file in the repo.
# They should be uploaded to GitHub releases instead.
main.js
# Exclude sourcemaps
*.map
# obsidian
data.json
# Exclude macOS Finder (System Explorer) View States
.DS_Store
webpage-html-export.zip
app-styles.css
webpage-html-export.code-workspace
*.bat
build
dist
todo.txt
vault/
output/

View File

@ -1 +0,0 @@
tag-version-prefix=""

View File

@ -1,51 +0,0 @@
FROM node:16-alpine AS Build
# copy the assets and source code
COPY . /app
WORKDIR /app
# install dependencies
RUN npm install
# build the app
RUN npm run build
FROM ubuntu:20.04 AS Run
# Set image parameters
ARG OBSIDIAN_VERSION=1.6.7
ARG DEBIAN_FRONTEND=noninteractive
VOLUME [ "/vault", "/output", "/config.json" ]
ENV TZ=Etc/UTC
# Install dependencies
RUN apt update
RUN apt install -y python3 python3-pip curl x11vnc xvfb tzdata jq
# Download the Obsidian package
RUN curl -L "https://github.com/obsidianmd/obsidian-releases/releases/download/v${OBSIDIAN_VERSION}/obsidian_${OBSIDIAN_VERSION}_amd64.deb" -o obsidian.deb
# Install patcher
RUN pip3 install electron-inject
# Install Obsidian
RUN apt install -y ./obsidian.deb
# Copy build output
COPY --from=Build /app/main.js /plugin/main.js
COPY --from=Build /app/styles.css /plugin/styles.css
COPY --from=Build /app/manifest.json /plugin/manifest.json
# Copy the inject scripts
COPY docker/inject-enable.js /inject-enable.js
# Copy the run script
COPY docker/run.sh /run.sh
RUN chmod +x /run.sh
# Set up the vault
RUN mkdir -p /root/.config/obsidian
RUN mkdir /output
RUN echo '{"vaults":{"94349b4f2b2e057a":{"path":"/vault","ts":1715257568671,"open":true}}}' > /root/.config/obsidian/obsidian.json
CMD xvfb-run /run.sh

View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2023 Nathan George
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,65 +0,0 @@
# Webpage HTML Export
Export html from single files, canvas pages, or whole vaults. Direct access to the exported HTML files allows you to publish your digital garden anywhere. Focuses on flexibility, features, and style parity.
Demo / docs: [docs.obsidianweb.net](https://docs.obsidianweb.net/)
![image](https://github.com/KosmosisDire/obsidian-webpage-export/assets/39423700/b8e227e4-b12c-47fb-b341-5c5c2f092ffa)
![image](https://github.com/KosmosisDire/obsidian-webpage-export/assets/39423700/06f29e1a-c067-45e7-9882-f9d6aa83776f)
> [!NOTE]
> Although the plugin is fully functional it is still under development, so there may be frequent large changes between updates that could effect your workflow! Bugs are also not uncommon, please report anything you find, I am working to make the plugin more stable.
## Features:
- Full text search
- File navigation tree
- Document outline
- Graph view
- Theme toggle
- Optimized for web and mobile
- Most plugins supported (dataview, tasks, etc...)
- Option to export html and dependencies into one single file
## Using the Plugin
Check out the new docs for details on using the plugin:
https://docs.obsidianweb.net/
## Installation
Install from Obsidian Community Plugins: [Open in Obsidian](https://obsidian.md/plugins?id=webpage-html-export)
### Manual Installation
1. Download the `.zip` file from the [Latest Release](https://github.com/KosmosisDire/obsidian-webpage-export/releases/latest), or from any other release version.
2. Unzip into: `{VaultFolder}/.obsidian/plugins/`
3. Reload obsidian
### Beta Installation
Either follow the instructions above for a beta release, or:
1. Install the [BRAT plugin](https://obsidian.md/plugins?id=obsidian42-brat)
2. Open the brat settings
3. Select add beta plugin
4. Enter `https://github.com/KosmosisDire/obsidian-webpage-export` as the repository.
5. Select Add Plugin
## Contributing
Only start work on features which have an issue created for them and have been accepted by me!
A contribution guide may come soon.
## Support This Plugin
This plugin takes a lot of work to maintain and continue adding features. If you want to fund the continued development of this plugin you can do so here:
<a href="https://www.buymeacoffee.com/nathangeorge"><img src="https://img.buymeacoffee.com/button-api/?text=Buy me a coffee&emoji=&slug=nathangeorge&button_colour=6a8695&font_colour=ffffff&font_family=Poppins&outline_colour=000000&coffee_colour=FFDD00"></a>
or if you prefer paypal:
<a href="https://www.paypal.com/donate/?business=HHQBAXQQXT84Q&no_recurring=0&item_name=Hey+%F0%9F%91%8B+I+am+a+Computer+Science+student+working+on+obsidian+plugins.+Thanks+for+your+support%21&currency_code=USD"><img src="https://pics.paypal.com/00/s/MGNjZDA4MDItYzk3MC00NTQ1LTg4ZDAtMzM5MTc4ZmFlMGIy/file.PNG" style="width: 150px;"></a>
## Testing
This project is tested with BrowserStack.
[BrowserStack](https://www.browserstack.com/open-source) offers free web testing to open source projects, but does not support this project in any other way.

View File

@ -1,46 +0,0 @@
name: "Export Obsidian vault to HTML"
description: "Exports the Obsidian vault to HTML"
inputs:
vault:
description: "Path to the Obsidian vault"
required: true
default: "."
config:
description: "Path to the configuration file"
required: false
version:
description: "Version of the plugin"
required: false
default: "latest"
outputs:
output:
description: "Path to the output folder"
value: ${{ steps.run-docker.outputs.output }}
runs:
using: "composite"
steps:
- name: Run docker container
id: run-docker
shell: bash
run: |
set +e
mkdir -p ./.output
if [ -z "${{ inputs.config }}" ]; then
docker run --rm -v ${{ inputs.vault }}:/vault -v ./.output:/output KosmosisDire/obsidian-webpage-export:${{ inputs.version }} >> ./.log.txt
else
docker run --rm -v ${{ inputs.vault }}:/vault -v ${{ inputs.config }}:/config.json -v ./.output:/output KosmosisDire/obsidian-webpage-export:${{ inputs.version }} >> ./.log.txt
fi
cat ./.log.txt
if [ $? -eq 137 ]; then
exit 0
echo "output=./.output" >> $GITHUB_OUTPUT
fi

View File

@ -1,26 +0,0 @@
declare module "*.txt.js" {
const value: string
export default value
}
declare module "*.txt.css" {
const value: string
export default value
}
declare module "*.txt" {
const value: string
export default value
}
// binary
declare module "*.wasm" {
const value: Uint8Array
export default value
}
declare module "*.png" {
const value: Uint8Array
export default value
}

View File

@ -1,296 +0,0 @@
/* Define default values for variables */
body
{
--line-width: 40em;
--line-width-adaptive: 40em;
--file-line-width: 40em;
--sidebar-width: min(20em, 80vw);
--collapse-arrow-size: 11px;
--tree-horizontal-spacing: 0.6em;
--tree-vertical-spacing: 0.6em;
--sidebar-margin: 12px;
}
/*#region Sidebars */
.sidebar {
height: 100%;
min-width: calc(var(--sidebar-width) + var(--divider-width-hover));
max-width: calc(var(--sidebar-width) + var(--divider-width-hover));
font-size: 14px;
z-index: 10;
position: relative;
overflow: hidden;
/* overflow: clip; */
transition: min-width ease-in-out, max-width ease-in-out;
transition-duration: .2s;
contain: size;
}
.sidebar-left {
left: 0;
}
.sidebar-right {
right: 0;
}
.sidebar.is-collapsed {
min-width: 0;
max-width: 0;
}
body.floating-sidebars .sidebar {
position: absolute;
}
.sidebar-content {
height: 100%;
min-width: calc(var(--sidebar-width) - var(--divider-width-hover));
top: 0;
padding: var(--sidebar-margin);
padding-top: 4em;
line-height: var(--line-height-tight);
background-color: var(--background-secondary);
transition: background-color,border-right,border-left,box-shadow;
transition-duration: var(--color-fade-speed);
transition-timing-function: ease-in-out;
position: absolute;
display: flex;
flex-direction: column;
}
/* If the sidebar isn't collapsed the content should have the same width as it */
.sidebar:not(.is-collapsed) .sidebar-content {
min-width: calc(max(100%,var(--sidebar-width)) - 3px);
max-width: calc(max(100%,var(--sidebar-width)) - 3px);
}
.sidebar-left .sidebar-content
{
left: 0;
border-top-right-radius: var(--radius-l);
border-bottom-right-radius: var(--radius-l);
}
.sidebar-right .sidebar-content
{
right: 0;
border-top-left-radius: var(--radius-l);
border-bottom-left-radius: var(--radius-l);
}
/* Hide empty sidebars */
.sidebar:has(.sidebar-content:empty):has(.topbar-content:empty)
{
display: none;
}
.sidebar-topbar {
height: 2em;
width: var(--sidebar-width);
top: var(--sidebar-margin);
padding-inline: var(--sidebar-margin);
z-index: 1;
position: fixed;
display: flex;
align-items: center;
transition: width ease-in-out;
transition-duration: inherit;
}
.sidebar.is-collapsed .sidebar-topbar {
width: calc(2.3em + var(--sidebar-margin) * 2);
}
.sidebar .sidebar-topbar.is-collapsed
{
width: 0;
}
.sidebar-left .sidebar-topbar {
left: 0;
}
.sidebar-right .sidebar-topbar {
right: 0;
}
.topbar-content {
overflow: hidden;
overflow: clip;
width: 100%;
height: 100%;
display: flex;
align-items: center;
transition: inherit;
}
.sidebar.is-collapsed .topbar-content {
width: 0;
transition: inherit;
}
.clickable-icon.sidebar-collapse-icon {
background-color: transparent;
color: var(--icon-color-focused);
padding: 0!important;
margin: 0!important;
height: 100%!important;
width: 2.3em !important;
margin-inline: 0.14em!important;
position: absolute;
}
.sidebar-left .clickable-icon.sidebar-collapse-icon {
transform: rotateY(180deg);
right: var(--sidebar-margin);
}
.sidebar-right .clickable-icon.sidebar-collapse-icon {
transform: rotateY(180deg);
left: var(--sidebar-margin);
}
.clickable-icon.sidebar-collapse-icon svg.svg-icon {
width: 100%;
height: 100%;
}
.sidebar-section-header
{
margin: 0 0 1em 0;
text-transform: uppercase;
letter-spacing: 0.06em;
font-weight: 600;
}
/*#endregion */
/*#region Content / Markdown Preview View */
body
{
transition: background-color var(--color-fade-speed) ease-in-out;
}
.webpage-container {
display: flex;
flex-direction: row;
height: 100%;
width: 100%;
align-items: stretch;
justify-content: center;
}
.document-container
{
opacity: 1;
flex-basis: 100%;
max-width: 100%;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
transition: opacity 0.2s ease-in-out;
contain: inline-size;
}
.hide
{
opacity: 0;
transition: opacity 0.2s ease-in-out;
}
.document-container > .markdown-preview-view
{
margin: var(--sidebar-margin);
margin-bottom: 0;
width: 100%;
width: -webkit-fill-available;
width: -moz-available;
width: fill-available;
background-color: var(--background-primary);
transition: background-color var(--color-fade-speed) ease-in-out;
border-top-right-radius: var(--window-radius, var(--radius-m));
border-top-left-radius: var(--window-radius, var(--radius-m));
overflow-x: hidden !important;
overflow-y: auto !important;
display: flex !important;
flex-direction: column !important;
align-items: center !important;
contain: inline-size;
}
.document-container>.markdown-preview-view>.markdown-preview-sizer
{
padding-bottom: 80vh !important;
width: 100% !important;
max-width: var(--line-width) !important;
flex-basis: var(--line-width) !important;
transition: background-color var(--color-fade-speed) ease-in-out;
contain: inline-size;
}
.view-content img:not([width]), .markdown-rendered img:not([width])
{
max-width: 100%;
outline: none;
}
/* If the markdown view is displaying a raw file or embed then increase it's size to make everything as large as possible */
.document-container > .view-content.embed {
display: flex;
padding: 1em;
height: 100%;
width: 100%;
align-items: center;
justify-content: center;
}
.document-container > .view-content.embed > *
{
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
*:has(> :is(.math, table)) {
overflow-x: auto !important;
}
/* For custom view exports */
.document-container > .view-content
{
overflow-x: auto;
contain: content;
padding: 0;
margin: 0;
height: 100%;
}
/*#endregion */
/*#region Loading */
.scroll-highlight
{
position: absolute;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 1000;
background-color: hsla(var(--color-accent-hsl),.25);
opacity: 0;
padding: 1em;
inset: 50%;
translate: -50% -50%;
border-radius: var(--radius-s);
}
/*#endregion */

View File

@ -1,98 +0,0 @@
async function loadIncludes()
{
if (location.protocol != "file:")
{
// replace include tags with the contents of the file
let includeTags = document.querySelectorAll("include");
for (let i = 0; i < includeTags.length; i++)
{
let includeTag = includeTags[i];
let includePath = includeTag.getAttribute("src");
try
{
const request = await fetch(includePath);
if (!request.ok)
{
console.log("Could not include file: " + includePath);
includeTag?.remove();
continue;
}
let includeText = await request.text();
let docFrag = document.createRange().createContextualFragment(includeText);
let includeChildren = Array.from(docFrag.children);
for (let child of includeChildren)
{
child.classList.add("hide");
child.style.transition = "opacity 0.5s ease-in-out";
setTimeout(() =>
{
child.classList.remove("hide");
}, 10);
};
includeTag.before(docFrag);
includeTag.remove();
console.log("Included file: " + includePath);
}
catch (e)
{
includeTag?.remove();
console.log("Could not include file: " + includePath, e);
continue;
}
}
}
else
{
let e = document.querySelectorAll("include");
if (e.length > 0)
{
var error = document.createElement("div");
error.id = "error";
error.textContent = "Web server exports must be hosted on an http / web server to be viewed correctly.";
error.style.position = "fixed";
error.style.top = "50%";
error.style.left = "50%";
error.style.transform = "translate(-50%, -50%)";
error.style.fontSize = "1.5em";
error.style.fontWeight = "bold";
error.style.textAlign = "center";
document.body.appendChild(error);
document.querySelector(".document-container")?.classList.remove("hide");
}
}
}
document.addEventListener("DOMContentLoaded", () =>
{
loadIncludes();
});
let isFileProtocol = location.protocol == "file:";
function waitLoadScripts(scriptNames, callback)
{
let scripts = scriptNames.map(name => document.getElementById(name + "-script"));
let index = 0;
function loadNext()
{
let script = scripts[index];
index++;
if (!script || script.getAttribute('loaded') == "true") // if already loaded
{
if (index < scripts.length)
loadNext();
}
if (index < scripts.length) script.addEventListener("load", loadNext);
else callback();
}
loadNext();
}

View File

@ -1,417 +0,0 @@
// Import Pixi.js library
if( 'function' === typeof importScripts)
{
importScripts('https://d157l7jdn8e5sf.cloudfront.net/v7.2.0/webworker.js', './tinycolor.js');
addEventListener('message', onMessage);
let app;
let container;
let graphics;
isDrawing = false;
let linkCount = 0;
let linkSources = [];
let linkTargets = [];
let nodeCount = 0;
let radii = [];
let labels = [];
let labelFade = [];
let labelWidths = [];
let pixiLabels = [];
let cameraOffset = {x: 0, y: 0};
let positions = new Float32Array(0);
let linkLength = 0;
let edgePruning = 0;
let colors =
{
background: 0x232323,
link: 0xAAAAAA,
node: 0xCCCCCC,
outline: 0xAAAAAA,
text: 0xFFFFFF,
accent: 0x4023AA
}
let hoveredNode = -1;
let lastHoveredNode = -1;
let grabbedNode = -1;
let updateAttached = false;
let attachedToGrabbed = [];
let activeNode = -1;
let attachedToActive = [];
let cameraScale = 1;
let cameraScaleRoot = 1;
function toScreenSpace(x, y, floor = true)
{
if (floor)
{
return {x: Math.floor((x * cameraScale) + cameraOffset.x), y: Math.floor((y * cameraScale) + cameraOffset.y)};
}
else
{
return {x: (x * cameraScale) + cameraOffset.x, y: (y * cameraScale) + cameraOffset.y};
}
}
function vecToScreenSpace({x, y}, floor = true)
{
return toScreenSpace(x, y, floor);
}
function toWorldspace(x, y)
{
return {x: (x - cameraOffset.x) / cameraScale, y: (y - cameraOffset.y) / cameraScale};
}
function vecToWorldspace({x, y})
{
return toWorldspace(x, y);
}
function setCameraCenterWorldspace({x, y})
{
cameraOffset.x = (canvas.width / 2) - (x * cameraScale);
cameraOffset.y = (canvas.height / 2) - (y * cameraScale);
}
function getCameraCenterWorldspace()
{
return toWorldspace(canvas.width / 2, canvas.height / 2);
}
function getNodeScreenRadius(radius)
{
return radius * cameraScaleRoot;
}
function getNodeWorldspaceRadius(radius)
{
return radius / cameraScaleRoot;
}
function getPosition(index)
{
return {x: positions[index * 2], y: positions[index * 2 + 1]};
}
function mixColors(hexStart, hexEnd, factor)
{
return tinycolor.mix(tinycolor(hexStart.toString(16)), tinycolor(hexEnd.toString(16)), factor).toHexNumber()
}
function darkenColor(hexColor, factor)
{
return tinycolor(hexColor.toString(16)).darken(factor).toHexNumber();
}
function lightenColor(hexColor, factor)
{
return tinycolor(hexColor.toString(16)).lighten(factor).toHexNumber();
}
function invertColor(hex, bw)
{
hex = hex.toString(16); // force conversion
// fill extra space up to 6 characters with 0
while (hex.length < 6) hex = "0" + hex;
if (hex.indexOf('#') === 0) {
hex = hex.slice(1);
}
// convert 3-digit hex to 6-digits.
if (hex.length === 3) {
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
}
if (hex.length !== 6) {
throw new Error('Invalid HEX color:' + hex);
}
var r = parseInt(hex.slice(0, 2), 16),
g = parseInt(hex.slice(2, 4), 16),
b = parseInt(hex.slice(4, 6), 16);
if (bw) {
// https://stackoverflow.com/a/3943023/112731
return (r * 0.299 + g * 0.587 + b * 0.114) > 186
? '#000000'
: '#FFFFFF';
}
// invert color components
r = (255 - r).toString(16);
g = (255 - g).toString(16);
b = (255 - b).toString(16);
// pad each with zeros and return
return "#" + padZero(r) + padZero(g) + padZero(b);
}
function clamp(value, min, max)
{
return Math.min(Math.max(value, min), max);
}
function lerp(a, b, t)
{
return a + (b - a) * t;
}
let hoverFade = 0;
let hoverFadeSpeed = 0.2;
let hoverFontSize = 15;
let normalFontSize = 12;
let fontRatio = hoverFontSize / normalFontSize;
function showLabel(index, fade, hovered = false)
{
let label = pixiLabels[index];
if (!label) return;
labelFade[index] = fade;
if(fade > 0.01) label.visible = true;
else
{
hideLabel(index);
return;
}
if (hovered) label.style.fontSize = hoverFontSize;
else label.style.fontSize = normalFontSize;
let nodePos = vecToScreenSpace(getPosition(index));
let width = (labelWidths[index] * (hovered ? fontRatio : 1)) / 2;
label.x = nodePos.x - width;
label.y = nodePos.y + getNodeScreenRadius(radii[index]) + 9;
label.alpha = fade;
}
function hideLabel(index)
{
let label = pixiLabels[index];
label.visible = false;
}
function draw()
{
graphics.clear();
let topLines = [];
if (updateAttached)
{
attachedToGrabbed = [];
// hoverFade = 0;
}
if (hoveredNode != -1 || grabbedNode != -1)
{
hoverFade = Math.min(1, hoverFade + hoverFadeSpeed);
}
else
{
hoverFade = Math.max(0, hoverFade - hoverFadeSpeed);
}
graphics.lineStyle(1, mixColors(colors.link, colors.background, hoverFade * 50), 0.7);
for (let i = 0; i < linkCount; i++)
{
let target = linkTargets[i];
let source = linkSources[i];
if (hoveredNode == source || hoveredNode == target || ((lastHoveredNode == source || lastHoveredNode == target) && hoverFade != 0))
{
if (updateAttached && hoveredNode == source)
attachedToGrabbed.push(target);
else if (updateAttached && hoveredNode == target)
attachedToGrabbed.push(source);
topLines.push(i);
}
let startWorld = getPosition(source);
let endWorld = getPosition(target);
let start = vecToScreenSpace(startWorld);
let end = vecToScreenSpace(endWorld);
let dist = Math.sqrt(Math.pow(startWorld.x - endWorld.x, 2) + Math.pow(startWorld.y - endWorld.y, 2));
if (dist < (radii[source] + radii[target]) * edgePruning)
{
graphics.moveTo(start.x, start.y);
graphics.lineTo(end.x, end.y);
}
}
let opacity = 1 - (hoverFade * 0.5);
graphics.beginFill(mixColors(colors.node, colors.background, hoverFade * 50), opacity);
graphics.lineStyle(0, 0xffffff);
for (let i = 0; i < nodeCount; i++)
{
let screenRadius = getNodeScreenRadius(radii[i]);
if (hoveredNode != i)
{
if (screenRadius > 2)
{
let labelFade = lerp(0, (screenRadius - 4) / 8 - (1/cameraScaleRoot)/6 * 0.9, Math.max(1 - hoverFade, 0.2));
showLabel(i, labelFade);
}
else
{
hideLabel(i);
}
}
if (hoveredNode == i || (lastHoveredNode == i && hoverFade != 0) || (hoveredNode != -1 && attachedToGrabbed.includes(i))) continue;
let pos = vecToScreenSpace(getPosition(i));
graphics.drawCircle(pos.x, pos.y, screenRadius);
}
graphics.endFill();
opacity = hoverFade * 0.7;
graphics.lineStyle(1, mixColors(mixColors(colors.link, colors.accent, hoverFade * 100), colors.background, 20), opacity);
for (let i = 0; i < topLines.length; i++)
{
let target = linkTargets[topLines[i]];
let source = linkSources[topLines[i]];
// draw lines on top when hovered
let start = vecToScreenSpace(getPosition(source));
let end = vecToScreenSpace(getPosition(target));
graphics.moveTo(start.x, start.y);
graphics.lineTo(end.x, end.y);
}
if(hoveredNode != -1 || (lastHoveredNode != -1 && hoverFade != 0))
{
graphics.beginFill(mixColors(colors.node, colors.accent, hoverFade * 20), 0.9);
graphics.lineStyle(0, 0xffffff);
for (let i = 0; i < attachedToGrabbed.length; i++)
{
let point = attachedToGrabbed[i];
let pos = vecToScreenSpace(getPosition(point));
graphics.drawCircle(pos.x, pos.y, getNodeScreenRadius(radii[point]));
showLabel(point, Math.max(hoverFade * 0.6, labelFade[point]));
}
graphics.endFill();
let index = hoveredNode != -1 ? hoveredNode : lastHoveredNode;
let pos = vecToScreenSpace(getPosition(index));
graphics.beginFill(mixColors(colors.node, colors.accent, hoverFade * 100), 1);
graphics.lineStyle(hoverFade, mixColors(invertColor(colors.background, true), colors.accent, 50));
graphics.drawCircle(pos.x, pos.y, getNodeScreenRadius(radii[index]));
graphics.endFill();
showLabel(index, Math.max(hoverFade, labelFade[index]), true);
}
updateAttached = false;
graphics.lineStyle(2, colors.accent);
// draw the active node
if (activeNode != -1)
{
let pos = vecToScreenSpace(getPosition(activeNode));
graphics.drawCircle(pos.x, pos.y, getNodeScreenRadius(radii[activeNode]) + 4);
}
}
function onMessage(event)
{
if(event.data.type == "draw")
{
positions = new Float32Array(event.data.positions);
draw();
}
else if(event.data.type == "update_camera")
{
cameraOffset = event.data.cameraOffset;
cameraScale = event.data.cameraScale;
cameraScaleRoot = Math.sqrt(cameraScale);
}
else if(event.data.type == "update_interaction")
{
if(hoveredNode != event.data.hoveredNode && event.data.hoveredNode != -1) updateAttached = true;
if(grabbedNode != event.data.grabbedNode && event.data.hoveredNode != -1) updateAttached = true;
if(event.data.hoveredNode == -1) lastHoveredNode = hoveredNode;
else lastHoveredNode = -1;
hoveredNode = event.data.hoveredNode;
grabbedNode = event.data.grabbedNode;
}
else if(event.data.type == "resize")
{
app.renderer.resize(event.data.width, event.data.height);
}
else if(event.data.type == "set_active")
{
activeNode = event.data.active;
}
else if(event.data.type == "update_colors")
{
colors = event.data.colors;
for (let label of pixiLabels)
{
label.style.fill = invertColor(colors.background, true);
}
}
else if(event.data.type == "init")
{
// Extract data from message
linkCount = event.data.linkCount;
linkSources = event.data.linkSources;
linkTargets = event.data.linkTargets;
nodeCount = event.data.nodeCount;
radii = event.data.radii;
labels = event.data.labels;
linkLength = event.data.linkLength;
edgePruning = event.data.edgePruning;
app = new PIXI.Application({... event.data.options, antialias: true, resolution: 2, backgroundAlpha: 0, transparent: true});
container = new PIXI.Container();
graphics = new PIXI.Graphics();
app.stage.addChild(container);
container.addChild(graphics);
pixiLabels = [];
for (let i = 0; i < nodeCount; i++)
{
let label = new PIXI.Text(labels[i], {fontFamily : 'Arial', fontSize: 12, fontWeight: "normal", fill : invertColor(colors.background, true), align : 'center', anchor: 0.5});
pixiLabels.push(label);
labelWidths.push(label.width);
labelFade.push(0);
app.stage.addChild(label);
}
}
else
{
console.log("Unknown message type sent to graph worker: " + event.data.type);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,777 +0,0 @@
// Wasm glue
var Module = typeof Module != "undefined" ? Module : {};
var moduleOverrides = Object.assign({}, Module);
var arguments_ = [];
var thisProgram = "./this.program";
var quit_ = (status,toThrow)=>{
throw toThrow
}
;
var ENVIRONMENT_IS_WEB = typeof window == "object";
var ENVIRONMENT_IS_WORKER = typeof importScripts == "function";
var ENVIRONMENT_IS_NODE = typeof process == "object" && typeof process.versions == "object" && typeof process.versions.node == "string";
var scriptDirectory = "";
function locateFile(path) {
if (Module["locateFile"]) {
return Module["locateFile"](path, scriptDirectory)
}
return scriptDirectory + path
}
var read_, readAsync, readBinary, setWindowTitle;
if (ENVIRONMENT_IS_NODE) {
var fs = require("fs");
var nodePath = require("path");
if (ENVIRONMENT_IS_WORKER) {
scriptDirectory = nodePath.dirname(scriptDirectory) + "/"
} else {
scriptDirectory = __dirname + "/"
}
read_ = (filename,binary)=>{
filename = isFileURI(filename) ? new URL(filename) : nodePath.normalize(filename);
return fs.readFileSync(filename, binary ? undefined : "utf8")
}
;
readBinary = filename=>{
var ret = read_(filename, true);
if (!ret.buffer) {
ret = new Uint8Array(ret)
}
return ret
}
;
readAsync = (filename,onload,onerror)=>{
filename = isFileURI(filename) ? new URL(filename) : nodePath.normalize(filename);
fs.readFile(filename, function(err, data) {
if (err)
onerror(err);
else
onload(data.buffer)
})
}
;
if (!Module["thisProgram"] && process.argv.length > 1) {
thisProgram = process.argv[1].replace(/\\/g, "/")
}
arguments_ = process.argv.slice(2);
if (typeof module != "undefined") {
module["exports"] = Module
}
process.on("uncaughtException", function(ex) {
if (ex !== "unwind" && !(ex instanceof ExitStatus) && !(ex.context instanceof ExitStatus)) {
throw ex
}
});
var nodeMajor = process.versions.node.split(".")[0];
if (nodeMajor < 15) {
process.on("unhandledRejection", function(reason) {
throw reason
})
}
quit_ = (status,toThrow)=>{
process.exitCode = status;
throw toThrow
}
;
Module["inspect"] = function() {
return "[Emscripten Module object]"
}
} else if (ENVIRONMENT_IS_WEB || ENVIRONMENT_IS_WORKER) {
if (ENVIRONMENT_IS_WORKER) {
scriptDirectory = self.location.href
} else if (typeof document != "undefined" && document.currentScript) {
scriptDirectory = document.currentScript.src
}
if (scriptDirectory.indexOf("blob:") !== 0) {
scriptDirectory = scriptDirectory.substr(0, scriptDirectory.replace(/[?#].*/, "").lastIndexOf("/") + 1)
} else {
scriptDirectory = ""
}
{
read_ = url=>{
var xhr = new XMLHttpRequest;
xhr.open("GET", url, false);
xhr.send(null);
return xhr.responseText
}
;
if (ENVIRONMENT_IS_WORKER) {
readBinary = url=>{
var xhr = new XMLHttpRequest;
xhr.open("GET", url, false);
xhr.responseType = "arraybuffer";
xhr.send(null);
return new Uint8Array(xhr.response)
}
}
readAsync = (url,onload,onerror)=>{
var xhr = new XMLHttpRequest;
xhr.open("GET", url, true);
xhr.responseType = "arraybuffer";
xhr.onload = ()=>{
if (xhr.status == 200 || xhr.status == 0 && xhr.response) {
onload(xhr.response);
return
}
onerror()
}
;
xhr.onerror = onerror;
xhr.send(null)
}
}
setWindowTitle = title=>document.title = title
} else {}
var out = Module["print"] || console.log.bind(console);
var err = Module["printErr"] || console.warn.bind(console);
Object.assign(Module, moduleOverrides);
moduleOverrides = null;
if (Module["arguments"])
arguments_ = Module["arguments"];
if (Module["thisProgram"])
thisProgram = Module["thisProgram"];
if (Module["quit"])
quit_ = Module["quit"];
var wasmBinary;
if (Module["wasmBinary"])
wasmBinary = Module["wasmBinary"];
var noExitRuntime = Module["noExitRuntime"] || true;
if (typeof WebAssembly != "object") {
abort("no native wasm support detected")
}
var wasmMemory;
var ABORT = false;
var EXITSTATUS;
var HEAP8, HEAPU8, HEAP16, HEAPU16, HEAP32, HEAPU32, HEAPF32, HEAPF64;
function updateMemoryViews() {
var b = wasmMemory.buffer;
Module["HEAP8"] = HEAP8 = new Int8Array(b);
Module["HEAP16"] = HEAP16 = new Int16Array(b);
Module["HEAP32"] = HEAP32 = new Int32Array(b);
Module["HEAPU8"] = HEAPU8 = new Uint8Array(b);
Module["HEAPU16"] = HEAPU16 = new Uint16Array(b);
Module["HEAPU32"] = HEAPU32 = new Uint32Array(b);
Module["HEAPF32"] = HEAPF32 = new Float32Array(b);
Module["HEAPF64"] = HEAPF64 = new Float64Array(b)
}
var wasmTable;
var __ATPRERUN__ = [];
var __ATINIT__ = [];
var __ATPOSTRUN__ = [];
var runtimeInitialized = false;
function preRun() {
if (Module["preRun"]) {
if (typeof Module["preRun"] == "function")
Module["preRun"] = [Module["preRun"]];
while (Module["preRun"].length) {
addOnPreRun(Module["preRun"].shift())
}
}
callRuntimeCallbacks(__ATPRERUN__)
}
function initRuntime() {
runtimeInitialized = true;
callRuntimeCallbacks(__ATINIT__)
}
function postRun() {
if (Module["postRun"]) {
if (typeof Module["postRun"] == "function")
Module["postRun"] = [Module["postRun"]];
while (Module["postRun"].length) {
addOnPostRun(Module["postRun"].shift())
}
}
callRuntimeCallbacks(__ATPOSTRUN__)
}
function addOnPreRun(cb) {
__ATPRERUN__.unshift(cb)
}
function addOnInit(cb) {
__ATINIT__.unshift(cb)
}
function addOnPostRun(cb) {
__ATPOSTRUN__.unshift(cb)
}
var runDependencies = 0;
var runDependencyWatcher = null;
var dependenciesFulfilled = null;
function addRunDependency(id) {
runDependencies++;
if (Module["monitorRunDependencies"]) {
Module["monitorRunDependencies"](runDependencies)
}
}
function removeRunDependency(id) {
runDependencies--;
if (Module["monitorRunDependencies"]) {
Module["monitorRunDependencies"](runDependencies)
}
if (runDependencies == 0) {
if (runDependencyWatcher !== null) {
clearInterval(runDependencyWatcher);
runDependencyWatcher = null
}
if (dependenciesFulfilled) {
var callback = dependenciesFulfilled;
dependenciesFulfilled = null;
callback()
}
}
}
function abort(what) {
if (Module["onAbort"]) {
Module["onAbort"](what)
}
what = "Aborted(" + what + ")";
err(what);
ABORT = true;
EXITSTATUS = 1;
what += ". Build with -sASSERTIONS for more info.";
var e = new WebAssembly.RuntimeError(what);
throw e
}
var dataURIPrefix = "data:application/octet-stream;base64,";
function isDataURI(filename) {
return filename.startsWith(dataURIPrefix)
}
function isFileURI(filename) {
return filename.startsWith("file://")
}
var wasmBinaryFile;
wasmBinaryFile = "graph-wasm.wasm";
if (!isDataURI(wasmBinaryFile)) {
wasmBinaryFile = locateFile(wasmBinaryFile)
}
function getBinary(file) {
try {
if (file == wasmBinaryFile && wasmBinary) {
return new Uint8Array(wasmBinary)
}
if (readBinary) {
return readBinary(file)
}
throw "both async and sync fetching of the wasm failed"
} catch (err) {
abort(err)
}
}
function getBinaryPromise(binaryFile) {
if (!wasmBinary && (ENVIRONMENT_IS_WEB || ENVIRONMENT_IS_WORKER)) {
if (typeof fetch == "function" && !isFileURI(binaryFile)) {
return fetch(binaryFile, {
credentials: "same-origin"
}).then(function(response) {
if (!response["ok"]) {
throw "failed to load wasm binary file at '" + binaryFile + "'"
}
return response["arrayBuffer"]()
}).catch(function() {
return getBinary(binaryFile)
})
} else {
if (readAsync) {
return new Promise(function(resolve, reject) {
readAsync(binaryFile, function(response) {
resolve(new Uint8Array(response))
}, reject)
}
)
}
}
}
return Promise.resolve().then(function() {
return getBinary(binaryFile)
})
}
function instantiateArrayBuffer(binaryFile, imports, receiver) {
return getBinaryPromise(binaryFile).then(function(binary) {
return WebAssembly.instantiate(binary, imports)
}).then(function(instance) {
return instance
}).then(receiver, function(reason) {
err("failed to asynchronously prepare wasm: " + reason);
abort(reason)
})
}
function instantiateAsync(binary, binaryFile, imports, callback) {
if (!binary && typeof WebAssembly.instantiateStreaming == "function" && !isDataURI(binaryFile) && !isFileURI(binaryFile) && !ENVIRONMENT_IS_NODE && typeof fetch == "function") {
return fetch(binaryFile, {
credentials: "same-origin"
}).then(function(response)
{
let responseClone = new Response(response.body, { headers: new Headers({"Content-Type": "application/wasm"}) });
var result = WebAssembly.instantiateStreaming(responseClone, imports);
return result.then(callback, function(reason) {
err("wasm streaming compile failed: " + reason);
err("falling back to ArrayBuffer instantiation");
return instantiateArrayBuffer(binaryFile, imports, callback)
})
})
} else {
return instantiateArrayBuffer(binaryFile, imports, callback)
}
}
function createWasm() {
var info = {
"a": wasmImports
};
function receiveInstance(instance, module) {
var exports = instance.exports;
Module["asm"] = exports;
wasmMemory = Module["asm"]["f"];
updateMemoryViews();
wasmTable = Module["asm"]["r"];
addOnInit(Module["asm"]["g"]);
removeRunDependency("wasm-instantiate");
return exports
}
addRunDependency("wasm-instantiate");
function receiveInstantiationResult(result) {
receiveInstance(result["instance"])
}
if (Module["instantiateWasm"]) {
try {
return Module["instantiateWasm"](info, receiveInstance)
} catch (e) {
err("Module.instantiateWasm callback failed with error: " + e);
return false
}
}
instantiateAsync(wasmBinary, wasmBinaryFile, info, receiveInstantiationResult);
return {}
}
var tempDouble;
var tempI64;
var ASM_CONSTS = {
2304: $0=>{
console.log(UTF8ToString($0))
}
};
function ExitStatus(status) {
this.name = "ExitStatus";
this.message = "Program terminated with exit(" + status + ")";
this.status = status
}
function callRuntimeCallbacks(callbacks) {
while (callbacks.length > 0) {
callbacks.shift()(Module)
}
}
function getValue(ptr, type="i8") {
if (type.endsWith("*"))
type = "*";
switch (type) {
case "i1":
return HEAP8[ptr >> 0];
case "i8":
return HEAP8[ptr >> 0];
case "i16":
return HEAP16[ptr >> 1];
case "i32":
return HEAP32[ptr >> 2];
case "i64":
return HEAP32[ptr >> 2];
case "float":
return HEAPF32[ptr >> 2];
case "double":
return HEAPF64[ptr >> 3];
case "*":
return HEAPU32[ptr >> 2];
default:
abort("invalid type for getValue: " + type)
}
}
function setValue(ptr, value, type="i8") {
if (type.endsWith("*"))
type = "*";
switch (type) {
case "i1":
HEAP8[ptr >> 0] = value;
break;
case "i8":
HEAP8[ptr >> 0] = value;
break;
case "i16":
HEAP16[ptr >> 1] = value;
break;
case "i32":
HEAP32[ptr >> 2] = value;
break;
case "i64":
tempI64 = [value >>> 0, (tempDouble = value,
+Math.abs(tempDouble) >= 1 ? tempDouble > 0 ? (Math.min(+Math.floor(tempDouble / 4294967296), 4294967295) | 0) >>> 0 : ~~+Math.ceil((tempDouble - +(~~tempDouble >>> 0)) / 4294967296) >>> 0 : 0)],
HEAP32[ptr >> 2] = tempI64[0],
HEAP32[ptr + 4 >> 2] = tempI64[1];
break;
case "float":
HEAPF32[ptr >> 2] = value;
break;
case "double":
HEAPF64[ptr >> 3] = value;
break;
case "*":
HEAPU32[ptr >> 2] = value;
break;
default:
abort("invalid type for setValue: " + type)
}
}
function _abort() {
abort("")
}
var readEmAsmArgsArray = [];
function readEmAsmArgs(sigPtr, buf) {
readEmAsmArgsArray.length = 0;
var ch;
buf >>= 2;
while (ch = HEAPU8[sigPtr++]) {
buf += ch != 105 & buf;
readEmAsmArgsArray.push(ch == 105 ? HEAP32[buf] : HEAPF64[buf++ >> 1]);
++buf
}
return readEmAsmArgsArray
}
function runEmAsmFunction(code, sigPtr, argbuf) {
var args = readEmAsmArgs(sigPtr, argbuf);
return ASM_CONSTS[code].apply(null, args)
}
function _emscripten_asm_const_int(code, sigPtr, argbuf) {
return runEmAsmFunction(code, sigPtr, argbuf)
}
function _emscripten_date_now() {
return Date.now()
}
function _emscripten_memcpy_big(dest, src, num) {
HEAPU8.copyWithin(dest, src, src + num)
}
function getHeapMax() {
return 2147483648
}
function emscripten_realloc_buffer(size) {
var b = wasmMemory.buffer;
try {
wasmMemory.grow(size - b.byteLength + 65535 >>> 16);
updateMemoryViews();
return 1
} catch (e) {}
}
function _emscripten_resize_heap(requestedSize) {
var oldSize = HEAPU8.length;
requestedSize = requestedSize >>> 0;
var maxHeapSize = getHeapMax();
if (requestedSize > maxHeapSize) {
return false
}
let alignUp = (x,multiple)=>x + (multiple - x % multiple) % multiple;
for (var cutDown = 1; cutDown <= 4; cutDown *= 2) {
var overGrownHeapSize = oldSize * (1 + .2 / cutDown);
overGrownHeapSize = Math.min(overGrownHeapSize, requestedSize + 100663296);
var newSize = Math.min(maxHeapSize, alignUp(Math.max(requestedSize, overGrownHeapSize), 65536));
var replacement = emscripten_realloc_buffer(newSize);
if (replacement) {
return true
}
}
return false
}
function getCFunc(ident) {
var func = Module["_" + ident];
return func
}
function writeArrayToMemory(array, buffer) {
HEAP8.set(array, buffer)
}
function lengthBytesUTF8(str) {
var len = 0;
for (var i = 0; i < str.length; ++i) {
var c = str.charCodeAt(i);
if (c <= 127) {
len++
} else if (c <= 2047) {
len += 2
} else if (c >= 55296 && c <= 57343) {
len += 4;
++i
} else {
len += 3
}
}
return len
}
function stringToUTF8Array(str, heap, outIdx, maxBytesToWrite) {
if (!(maxBytesToWrite > 0))
return 0;
var startIdx = outIdx;
var endIdx = outIdx + maxBytesToWrite - 1;
for (var i = 0; i < str.length; ++i) {
var u = str.charCodeAt(i);
if (u >= 55296 && u <= 57343) {
var u1 = str.charCodeAt(++i);
u = 65536 + ((u & 1023) << 10) | u1 & 1023
}
if (u <= 127) {
if (outIdx >= endIdx)
break;
heap[outIdx++] = u
} else if (u <= 2047) {
if (outIdx + 1 >= endIdx)
break;
heap[outIdx++] = 192 | u >> 6;
heap[outIdx++] = 128 | u & 63
} else if (u <= 65535) {
if (outIdx + 2 >= endIdx)
break;
heap[outIdx++] = 224 | u >> 12;
heap[outIdx++] = 128 | u >> 6 & 63;
heap[outIdx++] = 128 | u & 63
} else {
if (outIdx + 3 >= endIdx)
break;
heap[outIdx++] = 240 | u >> 18;
heap[outIdx++] = 128 | u >> 12 & 63;
heap[outIdx++] = 128 | u >> 6 & 63;
heap[outIdx++] = 128 | u & 63
}
}
heap[outIdx] = 0;
return outIdx - startIdx
}
function stringToUTF8(str, outPtr, maxBytesToWrite) {
return stringToUTF8Array(str, HEAPU8, outPtr, maxBytesToWrite)
}
function stringToUTF8OnStack(str) {
var size = lengthBytesUTF8(str) + 1;
var ret = stackAlloc(size);
stringToUTF8(str, ret, size);
return ret
}
var UTF8Decoder = typeof TextDecoder != "undefined" ? new TextDecoder("utf8") : undefined;
function UTF8ArrayToString(heapOrArray, idx, maxBytesToRead) {
var endIdx = idx + maxBytesToRead;
var endPtr = idx;
while (heapOrArray[endPtr] && !(endPtr >= endIdx))
++endPtr;
if (endPtr - idx > 16 && heapOrArray.buffer && UTF8Decoder) {
return UTF8Decoder.decode(heapOrArray.subarray(idx, endPtr))
}
var str = "";
while (idx < endPtr) {
var u0 = heapOrArray[idx++];
if (!(u0 & 128)) {
str += String.fromCharCode(u0);
continue
}
var u1 = heapOrArray[idx++] & 63;
if ((u0 & 224) == 192) {
str += String.fromCharCode((u0 & 31) << 6 | u1);
continue
}
var u2 = heapOrArray[idx++] & 63;
if ((u0 & 240) == 224) {
u0 = (u0 & 15) << 12 | u1 << 6 | u2
} else {
u0 = (u0 & 7) << 18 | u1 << 12 | u2 << 6 | heapOrArray[idx++] & 63
}
if (u0 < 65536) {
str += String.fromCharCode(u0)
} else {
var ch = u0 - 65536;
str += String.fromCharCode(55296 | ch >> 10, 56320 | ch & 1023)
}
}
return str
}
function UTF8ToString(ptr, maxBytesToRead) {
return ptr ? UTF8ArrayToString(HEAPU8, ptr, maxBytesToRead) : ""
}
function ccall(ident, returnType, argTypes, args, opts) {
var toC = {
"string": str=>{
var ret = 0;
if (str !== null && str !== undefined && str !== 0) {
ret = stringToUTF8OnStack(str)
}
return ret
}
,
"array": arr=>{
var ret = stackAlloc(arr.length);
writeArrayToMemory(arr, ret);
return ret
}
};
function convertReturnValue(ret) {
if (returnType === "string") {
return UTF8ToString(ret)
}
if (returnType === "boolean")
return Boolean(ret);
return ret
}
var func = getCFunc(ident);
var cArgs = [];
var stack = 0;
if (args) {
for (var i = 0; i < args.length; i++) {
var converter = toC[argTypes[i]];
if (converter) {
if (stack === 0)
stack = stackSave();
cArgs[i] = converter(args[i])
} else {
cArgs[i] = args[i]
}
}
}
var ret = func.apply(null, cArgs);
function onDone(ret) {
if (stack !== 0)
stackRestore(stack);
return convertReturnValue(ret)
}
ret = onDone(ret);
return ret
}
function cwrap(ident, returnType, argTypes, opts) {
var numericArgs = !argTypes || argTypes.every(type=>type === "number" || type === "boolean");
var numericRet = returnType !== "string";
if (numericRet && numericArgs && !opts) {
return getCFunc(ident)
}
return function() {
return ccall(ident, returnType, argTypes, arguments, opts)
}
}
var wasmImports = {
"b": _abort,
"e": _emscripten_asm_const_int,
"d": _emscripten_date_now,
"c": _emscripten_memcpy_big,
"a": _emscripten_resize_heap
};
var asm = createWasm();
var ___wasm_call_ctors = function() {
return (___wasm_call_ctors = Module["asm"]["g"]).apply(null, arguments)
};
var _SetBatchFractionSize = Module["_SetBatchFractionSize"] = function() {
return (_SetBatchFractionSize = Module["_SetBatchFractionSize"] = Module["asm"]["h"]).apply(null, arguments)
}
;
var _SetAttractionForce = Module["_SetAttractionForce"] = function() {
return (_SetAttractionForce = Module["_SetAttractionForce"] = Module["asm"]["i"]).apply(null, arguments)
}
;
var _SetLinkLength = Module["_SetLinkLength"] = function() {
return (_SetLinkLength = Module["_SetLinkLength"] = Module["asm"]["j"]).apply(null, arguments)
}
;
var _SetRepulsionForce = Module["_SetRepulsionForce"] = function() {
return (_SetRepulsionForce = Module["_SetRepulsionForce"] = Module["asm"]["k"]).apply(null, arguments)
}
;
var _SetCentralForce = Module["_SetCentralForce"] = function() {
return (_SetCentralForce = Module["_SetCentralForce"] = Module["asm"]["l"]).apply(null, arguments)
}
;
var _SetDt = Module["_SetDt"] = function() {
return (_SetDt = Module["_SetDt"] = Module["asm"]["m"]).apply(null, arguments)
}
;
var _Init = Module["_Init"] = function() {
return (_Init = Module["_Init"] = Module["asm"]["n"]).apply(null, arguments)
}
;
var _Update = Module["_Update"] = function() {
return (_Update = Module["_Update"] = Module["asm"]["o"]).apply(null, arguments)
}
;
var _SetPosition = Module["_SetPosition"] = function() {
return (_SetPosition = Module["_SetPosition"] = Module["asm"]["p"]).apply(null, arguments)
}
;
var _FreeMemory = Module["_FreeMemory"] = function() {
return (_FreeMemory = Module["_FreeMemory"] = Module["asm"]["q"]).apply(null, arguments)
}
;
var ___errno_location = function() {
return (___errno_location = Module["asm"]["__errno_location"]).apply(null, arguments)
};
var _malloc = Module["_malloc"] = function() {
return (_malloc = Module["_malloc"] = Module["asm"]["s"]).apply(null, arguments)
}
;
var _free = Module["_free"] = function() {
return (_free = Module["_free"] = Module["asm"]["t"]).apply(null, arguments)
}
;
var stackSave = function() {
return (stackSave = Module["asm"]["u"]).apply(null, arguments)
};
var stackRestore = function() {
return (stackRestore = Module["asm"]["v"]).apply(null, arguments)
};
var stackAlloc = function() {
return (stackAlloc = Module["asm"]["w"]).apply(null, arguments)
};
var ___cxa_is_pointer_type = function() {
return (___cxa_is_pointer_type = Module["asm"]["__cxa_is_pointer_type"]).apply(null, arguments)
};
Module["cwrap"] = cwrap;
Module["setValue"] = setValue;
Module["getValue"] = getValue;
var calledRun;
dependenciesFulfilled = function runCaller() {
if (!calledRun)
run();
if (!calledRun)
dependenciesFulfilled = runCaller
}
;
function run() {
if (runDependencies > 0) {
return
}
preRun();
if (runDependencies > 0) {
return
}
function doRun() {
if (calledRun)
return;
calledRun = true;
Module["calledRun"] = true;
if (ABORT)
return;
initRuntime();
if (Module["onRuntimeInitialized"])
Module["onRuntimeInitialized"]();
postRun()
}
if (Module["setStatus"]) {
Module["setStatus"]("Running...");
setTimeout(function() {
setTimeout(function() {
Module["setStatus"]("")
}, 1);
doRun()
}, 1)
} else {
doRun()
}
}
if (Module["preInit"]) {
if (typeof Module["preInit"] == "function")
Module["preInit"] = [Module["preInit"]];
while (Module["preInit"].length > 0) {
Module["preInit"].pop()()
}
}
try
{
run();
}
catch(e)
{
console.error(e);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

File diff suppressed because one or more lines are too long

View File

@ -1,188 +0,0 @@
.markdown-preview-view .heading-collapse-indicator
{
margin-left: calc( 0px - var(--collapse-arrow-size) - 10px) !important;
padding: 0px 0px !important;
}
.node-insert-event
{
animation-duration: unset !important;
animation-name: none !important;
}
hr
{
border: none;
border-top: var(--hr-thickness) solid;
border-color: var(--hr-color);
}
h1:hover .collapse-indicator, h2:hover .collapse-indicator, h3:hover .collapse-indicator, h4:hover .collapse-indicator, h5:hover .collapse-indicator, h6:hover .collapse-indicator, .collapse-indicator:hover, .is-collapsed .collapse-indicator, .cm-fold-indicator.is-collapsed .collapse-indicator, .cm-gutterElement:hover .collapse-indicator, .cm-gutterElement .is-collapsed .collapse-indicator, .cm-line:hover .cm-fold-indicator .collapse-indicator, .fold-gutter.is-collapsed, .fold-gutter:hover, .metadata-properties-heading:hover .collapse-indicator {
opacity: 1;
transition: opacity 0.15s ease-in-out;
}
.collapse-indicator, .fold-gutter
{
opacity: 0;
transition: opacity 0.15s ease-in-out;
}
@media print
{
html body > :not(.print)
{
display: unset !important;
}
.collapse-indicator
{
display: none !important;
}
.is-collapsed > element > .collapse-indicator
{
display: unset !important;
}
}
/*#region Misc Hiding */
.mod-header .metadata-container
{
display: none !important;
}
/*#endregion */
/*#region Transclusions */
.markdown-embed .heading-collapse-indicator {
translate: -1em 0;
}
.markdown-embed.internal-embed.inline-embed .markdown-embed-content,
.markdown-embed.internal-embed.inline-embed .markdown-embed-content .markdown-preview-view
{
overflow: visible !important;
}
.markdown-embed-link
{
display: none !important;
}
/*#endregion */
/*#region Canvas */
.canvas-wrapper:not(.mod-readonly) .canvas-node-content.markdown-embed>.markdown-embed-content>.markdown-preview-view
{
user-select: text !important;
}
.canvas-card-menu {
display: none;
cursor: default !important;
}
.canvas-controls {
display: none;
cursor: default !important;
}
.canvas-background
{
pointer-events: visible !important;
cursor: grab !important;
}
.canvas-background:active
{
cursor: grabbing !important;
}
.canvas-node-connection-point
{
display: none;
cursor: default !important;
}
.canvas-node-content
{
backface-visibility: visible !important;
}
.canvas-menu-container {
display: none;
}
.canvas-node-content-blocker
{
cursor: pointer !important;
}
.canvas-wrapper
{
position: relative;
cursor: default !important;
}
.canvas-node-resizer
{
cursor: default !important;
}
.canvas-node-container
{
cursor: default !important;
}
/*#endregion */
/*#region Code Copy */
/* Make code block copy button fade in and out */
.markdown-rendered pre:not(:hover) > button.copy-code-button
{
display: unset;
opacity: 0;
}
.markdown-rendered pre:hover > button.copy-code-button
{
opacity: 1;
}
.markdown-rendered pre button.copy-code-button
{
transition: opacity 0.2s ease-in-out, width 0.3s ease-in-out, background-color 0.2s ease-in-out;
text-overflow: clip;
}
.markdown-rendered pre > button.copy-code-button:hover
{
background-color: var(--interactive-normal);
}
.markdown-rendered pre > button.copy-code-button:active
{
background-color: var(--interactive-hover);
box-shadow: var(--input-shadow);
transition: none;
}
/*#endregion */
/*#region Lists */
.webpage-container .is-collapsed .list-collapse-indicator svg.svg-icon,
.webpage-container .is-collapsed .collapse-indicator svg.svg-icon
{
color: var(--collapse-icon-color-collapsed);
}
/*#endregion */

File diff suppressed because one or more lines are too long

View File

@ -1,16 +0,0 @@
let theme = localStorage.getItem("theme") || (window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light");
if (theme == "dark")
{
document.body.classList.add("theme-dark");
document.body.classList.remove("theme-light");
}
else
{
document.body.classList.add("theme-light");
document.body.classList.remove("theme-dark");
}
if (window.innerWidth < 480) document.body.classList.add("is-phone");
else if (window.innerWidth < 768) document.body.classList.add("is-tablet");
else if (window.innerWidth < 1024) document.body.classList.add("is-small-screen");
else document.body.classList.add("is-large-screen");

View File

@ -1,36 +0,0 @@
advanced-pdf-export
custom-classes
file-tree-alternative
homepage
OA-file-hider
obsidian-asciimath
obsidian-discordrpc
obsidian-dynamic-background
obsidian-dynamic-toc
obsidian-excel-to-markdown-table
obsidian-full-calendar
obsidian-graphviz
obsidian-latex
obsidian-latex-suite
obsidian-minimal-settings
obsidian-plantuml
obsidian-prozen
obsidian-statusbar-pomo
obsidian-style-settings
obsidian-underline
settings-search
surfing
table-editor-obsidian
text-snippets-obsidian
webpage-html-export
lapel
obsidian-regex-replace
templater-obsidian
editing-toolbar
obsidian-admonition
obsidian-banners
obsidian-charts
obsidian-excalidraw-plugin
obsidian42-brat
obsidian-icon-folder
vscode-editor

File diff suppressed because one or more lines are too long

View File

@ -1,11 +0,0 @@
{
"compilerOptions": {
"target": "ES2015",
"module": "ES2015",
"moduleResolution": "node",
"sourceMap": true,
"skipLibCheck": true,
},
"include": ["webpage.ts"]
}

View File

@ -1,309 +0,0 @@
class Website
{
public static website: Website;
public static isLoaded: boolean = false;
public static loadedDocument: WebpageDocument | null = null;
public static bodyEl: HTMLElement;
public static webpageEl: HTMLElement;
public static async init()
{
Website.website = new Website();
window.addEventListener("load", () => Website.onInit());
}
private static onInit()
{
let docEl = document.querySelector(".document-container") as HTMLElement;
this.loadedDocument = new WebpageDocument(docEl, Website.website, DocumentType.Markdown);
this.bodyEl = document.body as HTMLElement;
this.webpageEl = document.querySelector(".webpage-container") as HTMLElement;
this.bodyEl.classList.toggle("loading", false);
this.bodyEl.classList.toggle("loaded", true);
this.isLoaded = true;
console.log("loaded");
}
}
Website.init();
class Header
{
public level: number;
public id: string;
public text: string;
public isCollapsed: boolean;
public isVisible: boolean = true;
public parent: Header | null;
public childHeaders: Header[];
public children: HTMLElement[];
public nextHeader: Header | null;
public previousHeader: Header | null;
public wrapperEl: HTMLElement;
public headingEl: HTMLHeadingElement;
public childrenEl: HTMLElement;
public collapseEl: HTMLElement | null;
public containingSizer: HTMLElement | null;
public document: WebpageDocument;
constructor(wrapperEl: HTMLElement, document: WebpageDocument)
{
this.wrapperEl = wrapperEl;
this.headingEl = wrapperEl.querySelector(".heading") as HTMLHeadingElement;
this.childrenEl = wrapperEl.querySelector(".heading-children") as HTMLElement;
this.collapseEl = wrapperEl.querySelector(".heading-collapse-indicator");
this.containingSizer = wrapperEl.closest(".markdown-preview-sizer") ?? wrapperEl.closest(".view-content");
if (this.headingEl == null || this.childrenEl == null) throw new Error("Invalid header element");
this.level = parseInt(this.headingEl.tagName.substring(1));
this.id = this.headingEl.id;
this.text = this.headingEl.textContent ?? "";
this.isCollapsed = this.wrapperEl.classList.contains("is-collapsed");
this.document = document;
this.document.headers.push(this);
this.childHeaders = [];
this.children = [];
this.childrenEl.childNodes.forEach((child) =>
{
if (child instanceof HTMLElement)
{
if(child.classList.contains("heading-wrapper"))
{
let header = new Header(child, document);
header.parent = this;
this.childHeaders.push(header);
}
this.children.push(child);
}
});
if (this.parent)
{
let index = this.parent.childHeaders.indexOf(this);
this.previousHeader = this.parent.childHeaders[index - 1] ?? null;
this.nextHeader = this.parent.childHeaders[index + 1] ?? null;
}
let localThis = this;
this.collapseEl?.addEventListener("click", function ()
{
localThis.toggle();
});
}
private collapseTimeout: number | null = null;
private collapseHeight: number = 0;
private forceShown: boolean = false;
public async collapse(collapse: boolean, openParents = true, instant = false)
{
if (openParents && !collapse)
{
if (this.parent) this.parent.collapse(false, true, instant);
}
let needsChange = this.isCollapsed != collapse;
if (!needsChange)
{
// if opening show the header
if (!collapse && this.document?.documentType == DocumentType.Canvas) this.show(true);
return;
}
if (this.collapseTimeout)
{
clearTimeout(this.collapseTimeout);
this.childrenEl.style.transitionDuration = "";
this.childrenEl.style.height = "";
this.wrapperEl.classList.toggle("is-animating", false);
}
if (collapse)
{
this.collapseHeight = this.childrenEl.offsetHeight + parseFloat(this.children[this.children.length - 1]?.style.marginBottom || "0");
// show all sibling headers after this one
// this is so that when the header slides down you aren't left with a blank space
let next = this.nextHeader;
while (next && this.document.documentType == DocumentType.Canvas)
{
let localNext = next;
// force show the sibling header for 500ms while this one is collapsing
localNext.show(false, true, true);
setTimeout(function()
{
localNext.forceShown = false;
}, 500);
next = next.nextHeader;
}
}
let height = this.collapseHeight;
this.childrenEl.style.height = height + "px";
// if opening show the header
if (!collapse && this.document.documentType == DocumentType.Canvas) this.show(true);
this.isCollapsed = collapse;
if (instant)
{
console.log("instant");
this.childrenEl.style.transitionDuration = "0s";
this.wrapperEl.classList.toggle("is-collapsed", collapse);
this.childrenEl.style.height = "";
this.childrenEl.style.transitionDuration = "";
let newTotalHeight = Array.from(this.containingSizer?.children ?? []).reduce((acc, cur: HTMLElement) => acc + cur.offsetHeight, 0);
if(this.containingSizer) this.containingSizer.style.minHeight = newTotalHeight + "px";
return;
}
// get the length of the height transition on heading container and wait for that time before not displaying the contents
let transitionDuration: string | number = getComputedStyle(this.childrenEl).transitionDuration;
if (transitionDuration.endsWith("s")) transitionDuration = parseFloat(transitionDuration);
else if (transitionDuration.endsWith("ms")) transitionDuration = parseFloat(transitionDuration) / 1000;
else transitionDuration = 0;
// multiply the duration by the height so that the transition is the same speed regardless of the height of the header
let transitionDurationMod = Math.min(transitionDuration * Math.sqrt(height) / 16, 0.5); // longest transition is 0.5s
this.childrenEl.style.transitionDuration = `${transitionDurationMod}s`;
if (collapse) this.childrenEl.style.height = "0px";
else this.childrenEl.style.height = height + "px";
this.wrapperEl.classList.toggle("is-animating", true);
this.wrapperEl.classList.toggle("is-collapsed", collapse);
let localThis = this;
setTimeout(function()
{
localThis.childrenEl.style.transitionDuration = "";
if(!collapse) localThis.childrenEl.style.height = "";
localThis.wrapperEl.classList.toggle("is-animating", false);
let newTotalHeight = Array.from(localThis.containingSizer?.children ?? []).reduce((acc, cur: HTMLElement) => acc + cur.offsetHeight, 0);
if(localThis.containingSizer) localThis.containingSizer.style.minHeight = newTotalHeight + "px";
}, transitionDurationMod * 1000);
}
/**Restores a hidden header back to it's normal function */
public show(showParents:boolean = false, showChildren:boolean = false, forceStay:boolean = false)
{
if (forceStay) this.forceShown = true;
if (showParents)
{
if (this.parent) this.parent.show(true, false, forceStay);
}
if (showChildren)
{
this.childHeaders.forEach((header) =>
{
header.show(false, true, forceStay);
});
}
if(this.isVisible || this.isCollapsed) return;
this.wrapperEl.classList.toggle("is-hidden", false);
this.wrapperEl.style.height = "";
this.wrapperEl.style.visibility = "";
this.isVisible = true;
}
public toggle(openParents = true)
{
this.collapse(!this.isCollapsed, openParents);
}
/**Hides everything in a header and then makes the header div take up the same space as the header element */
public hide()
{
if(this.forceShown) return;
if(!this.isVisible || this.isCollapsed) return;
if(this.wrapperEl.style.display == "none") return;
let height = this.wrapperEl.offsetHeight;
this.wrapperEl.classList.toggle("is-hidden", true);
if (height != 0) this.wrapperEl.style.height = height + "px";
this.wrapperEl.style.visibility = "hidden";
this.isVisible = false;
}
}
class Tree
{
}
class TreeItem
{
}
class Canvas
{
}
class Sidebar
{
}
class SidebarGutter
{
}
export enum DocumentType
{
Markdown,
Canvas,
Embed,
Excalidraw,
Kanban,
Other
}
class WebpageDocument
{
public headers: Header[];
public website: Website;
public documentType: DocumentType;
public documentEl: HTMLElement;
public constructor(documentEl: HTMLElement, website: Website, documentType: DocumentType)
{
this.documentEl = documentEl;
this.website = website;
this.documentType = documentType;
this.headers = [];
// only create top level headers, because headers create their own children
this.documentEl.querySelectorAll(".heading-wrapper:not(:is(.heading-children .heading-wrapper))").forEach((headerEl) =>
{
new Header(headerEl as HTMLElement, this); // headers add themselves to the document
});
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,12 +0,0 @@
services:
web:
build: .
ports:
- "5900:5900"
volumes:
- ./vault:/vault
- ./output:/output
- ./data.json:/config.json
dns: 8.8.8.8

View File

@ -1,22 +0,0 @@
setTimeout(async () => {
await this.app.plugins.setEnable(true);
await this.app.plugins.enablePlugin('webpage-html-export');
const plugin = await this.app.plugins.getPlugin('webpage-html-export');
try {
await plugin.exportDocker();
} catch(_) {
const temp = plugin.settings.exportPath;
plugin.settings.exportPath = '/output';
await this.app.commands.commands['webpage-html-export:export-html-vault'].callback()
plugin.settings.exportPath = temp;
}
const process = require('child_process');
process.exec('pkill -9 1');
}, 5000);

View File

@ -1,14 +0,0 @@
# Copy the plugin and config to the vault, inject script and start Obsidian on startup
mkdir -p /vault/.obsidian/plugins/webpage-html-export
if [ -f /config.json ]; then cp /config.json /vault/.obsidian/plugins/webpage-html-export/data.json; fi
if [ ! -f /vault/.obsidian/plugins/webpage-html-export/main.js ]; then
cp /plugin/* /vault/.obsidian/plugins/webpage-html-export/
else
sed -i 's|callback: () => {|callback: async () => {|1' /vault/.obsidian/plugins/webpage-html-export/main.js
sed -i 's|HTMLExporter.export(true)|await HTMLExporter.export(true)|1' /vault/.obsidian/plugins/webpage-html-export/main.js
fi
python3 -m electron_inject -r /inject-enable.js - obsidian --remote-allow-origins=* --no-sandbox --no-xshm --disable-dev-shm-usage --disable-gpu --disable-software-rasterizer --remote-debugging-port=37941
x11vnc -forever -nopw -create

View File

@ -1,50 +0,0 @@
import esbuild from "esbuild";
import process from "process";
import builtins from 'builtin-modules'
const banner =
`/*
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
if you want to view the source, please visit the github repository of this plugin
*/
`;
const prod = (process.argv[2] === 'production');
esbuild.build({
loader: {
'.txt.js': 'text',
'.txt.css': 'text',
'.wasm': 'binary',
'.png': 'binary',
},
banner: {
js: banner,
},
entryPoints: ['./scripts/main.ts'],
bundle: true,
external: [
'obsidian',
'electron',
'@codemirror/autocomplete',
'@codemirror/collab',
'@codemirror/commands',
'@codemirror/language',
'@codemirror/lint',
'@codemirror/search',
'@codemirror/state',
'@codemirror/view',
'@lezer/common',
'@lezer/highlight',
'@lezer/lr',
'node:buffer',
'node:stream',
...builtins],
format: 'cjs',
watch: !prod,
target: 'es2018',
logLevel: "info",
sourcemap: prod ? false : 'inline',
treeShaking: true,
outfile: 'main.js'
}).catch(() => process.exit(1));

View File

@ -1,12 +0,0 @@
{
"id": "webpage-html-export",
"name": "Webpage HTML Export",
"version": "1.9.0-3b",
"minAppVersion": "1.6.0",
"description": "Export full websites or html documents from single files, canvas pages, or whole vaults. Similar to obsidian's publish or digital garden, but with direct access to the exported HTML. Focuses on flexibility, features, and style parity.",
"author": "Nathan George",
"authorUrl": "https://github.com/KosmosisDire/obsidian-webpage-export",
"isDesktopOnly": true,
"fundingUrl": "https://www.buymeacoffee.com/nathangeorge",
"updateNote": ""
}

View File

@ -1,12 +0,0 @@
{
"id": "webpage-html-export",
"name": "Webpage HTML Export",
"version": "1.8.01",
"minAppVersion": "1.4.0",
"description": "Export html from single files, canvas pages, or whole vaults. Direct access to the exported HTML files allows you to publish your digital garden anywhere. Focuses on flexibility, features, and style parity.",
"author": "Nathan George",
"authorUrl": "https://github.com/KosmosisDire/obsidian-webpage-export",
"isDesktopOnly": true,
"fundingUrl": "https://www.buymeacoffee.com/nathangeorge",
"updateNote": "This is a quick patch to fix the style issue\ncaused by the obsidian 1.5.8 update.\nIt is not newer than the current 1.8.1 beta."
}

File diff suppressed because it is too large Load Diff

View File

@ -1,35 +0,0 @@
{
"name": "obsidian-webpage-export",
"version": "1.3.2",
"description": "Exports an obsidian document as an HTML document / webpage / website, correctly including all styling and formatting.",
"main": "main.js",
"scripts": {
"dev": "node esbuild.config.mjs",
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
"version": "node version-bump.mjs && git add manifest.json versions.json"
},
"keywords": [],
"author": "Nathan George",
"license": "GPL-3.0",
"devDependencies": {
"@types/node": "^16.11.6",
"@types/rss": "^0.0.32",
"@typescript-eslint/eslint-plugin": "^7.8.0",
"@typescript-eslint/parser": "^7.8.0",
"builtin-modules": "3.3.0",
"electron": "^26.1.0",
"esbuild": "0.14.47",
"eslint": "^8.56.0",
"obsidian": "^1.4.11",
"tslib": "^2.6.2",
"typescript": "^5.4.5"
},
"dependencies": {
"file-type": "^19.0.0",
"html-minifier-terser": "^7.2.0",
"mime": "^4.0.3",
"minisearch": "^6.3.0",
"rss": "^1.2.2",
"upath": "^2.0.1"
}
}

View File

@ -1,30 +0,0 @@
<html>
<head>
<head-content />
<custom-head-content />
</head>
<body>
<vertical>
<webpage>
<left-sidebar>
<theme-toggle />
<file-viewer />
</left-sidebar>
<vertical>
<document />
</vertical>
<right-sidebar>
<graph-view />
<outline />
</right-sidebar>
</webpage>
</vertical>
</body>
</html>
<!-- Possible Tags:
HTML Tags: html, body, head
Layout Tags: vertical, horizontal
Main Element Tags: theme-toggle, file-viewer, document, graph-view, outline, head-content, custom-head-content
Website Layout Tags: webpage, left-sidebar, right-sidebar
-->

View File

@ -1,211 +0,0 @@
import { ExportPreset, Settings } from "./settings/settings";
/**
* General options for the MarkdownRendererAPI
*/
export class MarkdownRendererAPIOptions
{
/**
* The container to render the HTML into.
*/
container?: HTMLElement = undefined;
/**
* Keep the .markdown-preview-view or .view-content container elements.
*/
keepViewContainer?: boolean = true;
/**
* Convert the headers into a tree structure with all children of a header being in their own container.
*/
makeHeadersTrees?: boolean = true;
/**
* Run post processing on the html to clean up various obsidian specific elements.
*/
postProcess?: boolean = true;
/**
* Display a window with a log and progress bar.
*/
displayProgress?: boolean = true;
}
export class GraphViewOptions
{
attractionForce = 1;
linkLength = 10;
repulsionForce = 150;
centralForce = 3;
edgePruning = 100;
minNodeRadius = 3;
maxNodeRadius = 7;
}
/**
* Options for the MarkdownWebpageRendererAPI when export a webpage object.
*/
export class MarkdownWebpageRendererAPIOptions extends MarkdownRendererAPIOptions
{
/**
* Add sidebars to either side of the page.
*/
addSidebars?: boolean = Settings.exportPreset != ExportPreset.RawDocuments;
/**
* Add a theme toggle to the left sidebar.
*/
addThemeToggle?: boolean = Settings.addThemeToggle;
/**
* Add a file navigation tree to the left sidebar.
*/
addFileNavigation?: boolean = Settings.addFileNav;
/**
* Add a document outline to the right sidebar
*/
addOutline?: boolean = Settings.addOutline;
/**
* Add a search bar to the left sidebar.
*/
addSearch?: boolean = Settings.addSearchBar;
/**
* Add the global graph view to the right sidebar.
*/
addGraphView?: boolean = Settings.addGraphView;
/**
* Transfer body classes from obsidian to the exported document.
*/
addBodyClasses?: boolean = true;
/**
* Add mathjax styles to the document
*/
addMathjaxStyles?: boolean = true;
/**
* Add a <head> tag with metadata, scripts, and styles.
*/
addHeadTag?: boolean = true;
/**
* Create an RSS feed for the site
*/
addRSS?: boolean = Settings.addRSSFeed;
/**
* Add a title to the top of each page. (Makes sure there are no duplicate titles)
*/
addTitle?: boolean = Settings.addTitle;
/**
* The options controlling the behavior of the gaph view.
*/
graphViewOptions?: GraphViewOptions = new GraphViewOptions();
/**
* Allows lists with sub-items to be folded / collpased.
*/
allowFoldingLists?: boolean = Settings.allowFoldingLists;
/**
* Allows headings to be folded / collapsed.
*/
allowFoldingHeadings?: boolean = Settings.allowFoldingHeadings;
/**
* Allows the sidebars to be resized.
*/
allowResizeSidebars?: boolean = Settings.allowResizingSidebars;
/**
* The current file wil be opened in the file anvigation by default.
* This only works in HTML is inlined!
*/
openNavFileLocation?: boolean = true;
/**
* All items in the document outline will be collpased by default.
*/
startOutlineCollapsed?: boolean = Settings.startOutlineCollapsed;
/**
* Any outline item with a nesting level >= to this will be collapsible.
*/
minOutlineCollapsibleLevel?: number = Settings.minOutlineCollapse;
/**
* Include javascript in the export (both inline or external)
*/
includeJS?: boolean = true;
/**
* Include CSS in the export (both inline or external)
*/
includeCSS?: boolean = true;
/**
* Inline / embed media items (images, video, audio) directly into the HTML.
*/
inlineMedia?: boolean = Settings.inlineAssets;
/**
* Inline / embed the css styles directly into the HTML.
*/
inlineCSS?: boolean = Settings.inlineAssets;
/**
* Inline / embed the javascript directly into the HTML.
*/
inlineJS?: boolean = Settings.inlineAssets;
/**
* Inline / embed other HTML directly into the HTML.
*/
inlineHTML?: boolean = Settings.inlineAssets;
/**
* Inline / embed fonts directly into the HTML.
*/
inlineFonts?: boolean = Settings.inlineAssets;
/**
* Do not leave any online urls, download them and embed them into the HTML.
*/
offlineResources?: boolean = Settings.makeOfflineCompatible;
/**
* Make all paths and file names web style (lowercase, no spaces).
* For example: "My File.md" -> "my-file.html"
*/
webStylePaths?: boolean = Settings.makeNamesWebStyle;
/**
* Flatten all export paths so that all HTML files are exported to the same root directory without the normal folder structure.
*/
flattenExportPaths?: boolean = false;
/**
* Fix all links to be relative and direct to other files or media included in the export.
*/
fixLinks?: boolean = true;
/**
* The url that this site will be hosted at. This is used for the rss feed data.
*/
siteURL?: string = Settings.siteURL;
/**
* The name of the vault displayed above the file navigation.
*/
siteName?: string = Settings.vaultTitle || app.vault.getName();
/**
* The name of the author of the site.
*/
authorName?: string = Settings.authorName;
}

View File

@ -1,109 +0,0 @@
import { Notice, TFile, TFolder } from "obsidian";
import { Path } from "./utils/path";
import { Settings, SettingsPage } from "./settings/settings";
import HTMLExportPlugin from "./main";
import { Utils } from "./utils/utils";
import { Website } from "./objects/website";
import { MarkdownRendererAPI } from "./render-api";
export class HTMLExporter
{
public static async export(usePreviousSettings: boolean = true, overrideFiles: TFile[] | undefined = undefined, overrideExportPath: Path | undefined = undefined)
{
let info = await SettingsPage.updateSettings(usePreviousSettings, overrideFiles, overrideExportPath);
if ((!info && !usePreviousSettings) || (info && info.canceled)) return;
let files = info?.pickedFiles ?? overrideFiles ?? SettingsPage.getFilesToExport();
let exportPath = overrideExportPath ?? info?.exportPath ?? new Path(Settings.exportPath);
let website = await HTMLExporter.exportFiles(files, exportPath, true, Settings.deleteOldFiles);
if (!website) return;
if (Settings.openAfterExport) Utils.openPath(exportPath);
new Notice("✅ Finished HTML Export:\n\n" + exportPath, 5000);
}
public static async exportFiles(files: TFile[], destination: Path, saveFiles: boolean, deleteOld: boolean) : Promise<Website | undefined>
{
var website = await new Website().createWithFiles(files, destination);
if (!website)
{
new Notice("❌ Export Cancelled", 5000);
return;
}
await website.index.updateBodyClasses();
if (deleteOld) await website.index.deleteOldFiles();
if (saveFiles)
{
await Utils.downloadFiles(website.downloads, destination);
}
MarkdownRendererAPI.endBatch();
return website;
}
public static async exportFolder(folder: TFolder, rootExportPath: Path, saveFiles: boolean, clearDirectory: boolean) : Promise<Website | undefined>
{
let folderPath = new Path(folder.path);
let allFiles = HTMLExportPlugin.plugin.app.vault.getFiles();
let files = allFiles.filter((file) => new Path(file.path).directory.asString.startsWith(folderPath.asString));
return await this.exportFiles(files, rootExportPath, saveFiles, clearDirectory);
}
public static async exportVault(rootExportPath: Path, saveFiles: boolean, clearDirectory: boolean) : Promise<Website | undefined>
{
let files = HTMLExportPlugin.plugin.app.vault.getFiles();
return await this.exportFiles(files, rootExportPath, saveFiles, clearDirectory);
}
// public static async deleteNonExports(webpages: Webpage[], rootPath: Path)
// {
// return;
// // delete all files in root path that are not in exports
// let files = (await this.getAllFilesInFolderRecursive(rootPath)).filter((file) => !file.makeUnixStyle().asString.contains(Asset.mediaPath.makeUnixStyle().asString));
// RenderLog.log(files, "Deletion candidates");
// let toDelete = [];
// for (let i = 0; i < files.length; i++)
// {
// RenderLog.progress(i, files.length, "Finding Old Files", "Checking: " + files[i].asString, "var(--color-yellow)");
// let file = files[i];
// if(!webpages.find((exportedFile) => exportedFile.exportPathAbsolute.makeUnixStyle().asString == file.makeUnixStyle().asString))
// {
// for (let webpage of webpages)
// {
// if (webpage.downloads.find((download) => download.relativeDownloadDirectory.makeUnixStyle().asString == file.makeUnixStyle().asString))
// {
// toDelete.push(file);
// break;
// }
// }
// }
// }
// for (let i = 0; i < toDelete.length; i++)
// {
// let file = toDelete[i];
// RenderLog.progress(i, toDelete.length, "Deleting Old Files", "Deleting: " + file.asString, "var(--color-red)");
// await fs.unlink(file.asString);
// }
// // delete all empty folders in root path
// let folders = (await this.getAllEmptyFoldersRecursive(rootPath));
// for (let i = 0; i < folders.length; i++)
// {
// let folder = folders[i];
// RenderLog.progress(i, folders.length, "Deleting Empty Folders", "Deleting: " + folder.asString, "var(--color-purple)");
// await fs.rmdir(folder.directory.asString);
// }
// }
}

View File

@ -1,408 +0,0 @@
#include "Dense.h"
#include <vector>
#include <cstdlib>
#include <ctime>
#include <stdarg.h>
#include <algorithm>
#include <emscripten.h>
#include "SpatialHashTable.h"
using namespace Eigen;
Vector2f* positions;
Vector2f* connectedDeltas;
Vector2f* lastConnectedDeltas;
Vector2f* forces;
Vector2f massOffsetFromCenterAccumulator;
Vector2f massOffsetFromCenter;
float* radii;
int* linkSources;
int* linkTargets;
int* linkCounts;
float* maxRadiiLinks;
int totalNodes = 0;
int totalLinks = 0;
float maxRadius = 0;
float minRadius = 0;
float dt = 0.01f;
float attractionForce = 0;
float linkLength = 0;
float repulsionForce = 0;
float centralForce = 0;
float batchFractionSize = 1.0f;
int batchSize = 0;
int batchOffset = 0;
int nextBatchOffset = 0;
int batchesPerRound = 0;
int hoveredNode = -1;
SpatialHashingTable table = NULL;
float rand01()
{
return static_cast <float> (rand()) / static_cast <float> (RAND_MAX);
}
void print(const char* fmt, ...)
{
va_list arg;
va_start(arg, fmt);
char buffer[1024];
vsprintf(buffer, fmt, arg);
EM_ASM_ARGS({
console.log(UTF8ToString($0));
}, buffer);
va_end(arg);
}
void swap(Vector2f** a, Vector2f** b)
{
Vector2f *temp = *a;
*a = *b;
*b = temp;
}
float gaussianPlatau(float x, float width, float baseline = 0.1)
{
float x_wdith = x/width;
return (1-baseline) * 1/(1+x_wdith*x_wdith) + baseline;
}
extern "C"
{
EMSCRIPTEN_KEEPALIVE
void SetBatchFractionSize(float fraction)
{
batchFractionSize = fraction;
batchSize = std::min(std::max((int)(totalNodes * batchFractionSize), 50), totalNodes);
batchesPerRound = std::ceil(std::max(totalNodes / batchSize, 1));
}
EMSCRIPTEN_KEEPALIVE
void SetAttractionForce(float _attractionForce)
{
attractionForce = _attractionForce;
}
EMSCRIPTEN_KEEPALIVE
void SetLinkLength(float _linkLength)
{
linkLength = _linkLength;
}
EMSCRIPTEN_KEEPALIVE
void SetRepulsionForce(float _repulsionForce)
{
repulsionForce = _repulsionForce;
}
EMSCRIPTEN_KEEPALIVE
void SetCentralForce(float _centralForce)
{
centralForce = _centralForce;
}
EMSCRIPTEN_KEEPALIVE
void SetDt(float _dt)
{
dt = _dt;
}
EMSCRIPTEN_KEEPALIVE
void Init(Vector2f* _positions, float* _radii, int* _linkSources, int* _linkTargets, int _totalNodes, int _totalLinks, float batchFraction, float _dt, float _attractionForce, float _linkLength, float _repulsionForce, float _centralForce)
{
print("Init called");
totalNodes = _totalNodes;
totalLinks = _totalLinks;
batchFractionSize = batchFraction;
dt = _dt;
attractionForce = _attractionForce;
linkLength = _linkLength;
repulsionForce = _repulsionForce;
centralForce = _centralForce;
positions = _positions;
forces = new Vector2f[totalNodes];
connectedDeltas = new Vector2f[totalNodes];
lastConnectedDeltas = new Vector2f[totalNodes];
radii = _radii;
linkSources = _linkSources;
linkTargets = _linkTargets;
linkCounts = new int[totalNodes];
maxRadiiLinks = new float[totalNodes];
maxRadius = 0;
minRadius = 1000000;
for (int i = 0; i < totalNodes; i++)
{
maxRadius = std::max(maxRadius, radii[i]);
minRadius = std::min(minRadius, radii[i]);
maxRadiiLinks[i] = 0;
}
for (int i = 0; i < totalLinks; i++)
{
linkCounts[linkSources[i]]++;
linkCounts[linkTargets[i]]++;
maxRadiiLinks[linkSources[i]] = std::max(maxRadiiLinks[linkSources[i]], radii[linkTargets[i]]);
maxRadiiLinks[linkTargets[i]] = std::max(maxRadiiLinks[linkTargets[i]], radii[linkSources[i]]);
}
SetBatchFractionSize(batchFractionSize);
table = SpatialHashingTable(maxRadius);
}
float settleness = 1.0f;
EMSCRIPTEN_KEEPALIVE
int Update(float mouseX, float mouseY, int grabbedNode, float cameraScale)
{
Vector2f mousePos = Vector2f(mouseX, mouseY);
if(grabbedNode != -1)
{
settleness = 1.0f;
if(grabbedNode < totalNodes)
positions[grabbedNode] = mousePos;
else
{
print("Grabbed node is out of bounds");
}
}
swap(&connectedDeltas, &lastConnectedDeltas);
for (int i = 0; i < totalNodes; i++)
{
connectedDeltas[i] = Vector2f(0, 0);
forces[i] = Vector2f(0, 0);
}
float multiplier = multiplier = std::min(attractionForce, 1.0f);
float maxRadiusSqr = maxRadius * maxRadius;
for (int i = 0; i < totalLinks; i++)
{
int target = linkTargets[i];
int source = linkSources[i];
if (target == source) continue;
if(target >= totalNodes || source >= totalNodes)
{
print("Link target or source is out of bounds");
continue;
}
Vector2f targetPos = positions[target];
Vector2f sourcePos = positions[source];
Vector2f delta = targetPos - sourcePos;
connectedDeltas[source] += delta;
connectedDeltas[target] += -delta;
float sourceRadius = radii[source];
float targetRadius = radii[target];
float distance = delta.norm();
if (distance <= 1)
{
positions[source] += Vector2f((rand01() - 0.5) * (sourceRadius + targetRadius), (rand01() - 0.5) * (sourceRadius + targetRadius));
continue;
}
Vector2f normalizedDelta = delta / distance;
float distanceDelta = distance - sourceRadius - targetRadius - linkLength;
Vector2f sourceTargetPos = sourcePos + normalizedDelta * distanceDelta;
Vector2f targetSourcePos = targetPos - normalizedDelta * distanceDelta;
Vector2f sourceForce = (sourceTargetPos - sourcePos) * multiplier * (targetRadius / maxRadiiLinks[source]);
Vector2f targetForce = (targetSourcePos - targetPos) * multiplier * (sourceRadius / maxRadiiLinks[target]);
forces[target] += targetForce / linkCounts[target];
forces[source] += sourceForce / linkCounts[source];
}
std::srand(std::time(nullptr));
float settlnessAccumulator = 0.0f;
int repelEnd = std::min(batchOffset + std::max((int)ceil(batchSize * (log(50.0f * settleness)/1.7f)), 1), totalNodes);
bool foundHovered = false;
bool firstRun = true;
for (int i = batchOffset; i < repelEnd; i++)
{
nextBatchOffset = (i + 1) % totalNodes;
Vector2f nodePosition = positions[i];
float nodeR = radii[i];
Vector2f nodeForce = Vector2f::Zero();
for (int j = 0; j < totalNodes; j++)
{
if (firstRun)
{
// find hovered node
if (!foundHovered)
{
float dist = (positions[j] - mousePos).norm();
if (dist < radii[j] / sqrt(cameraScale))
{
hoveredNode = j;
foundHovered = true;
}
}
// if (j < batchOffset || j > repelEnd)
// {
// positions[j] += forces[j] * dt * settleness;
// }
massOffsetFromCenterAccumulator += positions[j];
// positions[j] -= massOffsetFromCenter.normalized() * dt;
}
if (i == j) continue;
Vector2f otherPosition = positions[j];
float otherR = radii[j];
Vector2f delta = nodePosition - otherPosition;
float distanceSqr = delta.squaredNorm();
if (distanceSqr <= 1)
{
positions[i] += Vector2f((rand01() - 0.5) * (nodeR + otherR), (rand01() - 0.5) * (nodeR + otherR));
continue;
}
float maxCube = maxRadius * maxRadius * maxRadius;
float otherCube = otherR * otherR * otherR;
float nodeCube = nodeR * nodeR * nodeR;
float force = (gaussianPlatau(sqrt(distanceSqr), 2.0f * (nodeR + otherR), 0.01f) * (otherCube/maxCube) / (nodeCube/maxCube) + (nodeR/maxRadius)) / (distanceSqr) * repulsionForce;
// force += 0.001f/sqrt(distanceSqr);
nodeForce += delta * force;
}
firstRun = false;
// center forces
float distance = nodePosition.norm();
float force = distance * centralForce/1000 * (nodeR/maxRadius) * (nodeR/maxRadius);
nodeForce += -nodePosition * force;
float batchFraction = batchFractionSize / 2;
nodeForce = Vector2f
(
(batchFraction) * nodeForce.x() + (1 - (batchFraction)) * forces[i].x(),
(batchFraction) * nodeForce.y() + (1 - (batchFraction)) * forces[i].y()
) * settleness;
if (nodeForce.norm() > maxRadiusSqr)
{
nodeForce = nodeForce / nodeForce.norm() * maxRadiusSqr;
}
forces[i] = nodeForce * 0.9f;
positions[i] += forces[i] * dt;
if(grabbedNode == i)
{
positions[grabbedNode] = mousePos;
}
settlnessAccumulator += ((connectedDeltas[i] - lastConnectedDeltas[i]).norm() + (connectedDeltas[i].normalized() - lastConnectedDeltas[i].normalized()).norm()) / 2;
}
batchOffset = nextBatchOffset;
settleness = settleness * 0.95f + std::min(settlnessAccumulator/totalNodes, 5.0f) * 0.01f;
massOffsetFromCenter = massOffsetFromCenterAccumulator / totalNodes;
if(grabbedNode != -1)
{
positions[grabbedNode] = mousePos;
return grabbedNode;
}
return foundHovered ? hoveredNode : -1;
}
EMSCRIPTEN_KEEPALIVE
void SetPosition(int index, float x, float y)
{
positions[index] = Vector2f(x, y);
}
void AbstractedUpdate()
{
std::vector<AbstractedGridspace> abstractedGrid = table.getAbstractedGrid(GridspacePositionCalculationMethod::WEIGHTED_AVERAGE, radii);
for (int i = 0; i < totalNodes; i++)
{
Vector2f nodePosition = positions[i];
float nodeRadius = radii[i];
for (int j = 0; j < abstractedGrid.size(); j++)
{
Vector2f gridSpacePosition = abstractedGrid[j].position;
float spaceStrength = abstractedGrid[j].weightsSum;
Vector2f delta = gridSpacePosition - nodePosition;
float distSqr = delta.dot(delta);
if (distSqr <= 0.00001)
{
positions[i] += Vector2f(1, 0);
continue;
}
float dist = sqrt(distSqr);
Vector2f normalizedDelta = delta / dist;
Vector2f force = normalizedDelta * repulsionForce * dt * spaceStrength;
positions[i] -= force;
}
Vector2f centralForceVector = -nodePosition.normalized() * centralForce;
positions[i] += centralForceVector * dt * settleness;
}
}
EMSCRIPTEN_KEEPALIVE
void FreeMemory()
{
delete[] forces;
delete[] linkCounts;
delete[] maxRadiiLinks;
delete[] connectedDeltas;
delete[] lastConnectedDeltas;
}
}

View File

@ -1,214 +0,0 @@
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include <Dense.h>
struct AbstractedGridspace {
Eigen::Vector2f position;
int numObjects;
int weightsSum;
};
enum class GridspacePositionCalculationMethod
{
AVERAGE,
WEIGHTED_AVERAGE,
CENTER
};
class SpatialHashingTable {
public:
SpatialHashingTable(float cellSize) : cellSize_(cellSize) {}
std::vector<int> getNearbyObjects(const Eigen::Vector2f& position, float radius) const
{
// Compute the set of cells that overlap with the circle defined by the radius and position
std::unordered_set<int> cells = findCells(position, radius);
// Collect all objects that belong to any of the overlapping cells
std::unordered_set<int> nearbyObjects;
for (int cell : cells) {
std::unordered_set objectsInCell = objects_.find(cell)->second;
nearbyObjects.insert(objectsInCell.begin(), objectsInCell.end());
}
return std::vector<int>(nearbyObjects.begin(), nearbyObjects.end());
}
std::vector<AbstractedGridspace> getAbstractedGrid(GridspacePositionCalculationMethod calcMethod, float* weights = nullptr)
{
std::vector<AbstractedGridspace> grid;
for (const auto& [cell, objects] : objects_)
{
Eigen::Vector2f center = computeCellCenter(cell, calcMethod, weights);
int numObjects = static_cast<int>(objects.size());
int weightsSum = 0;
for (int object : objects) {
weightsSum += weights[object];
}
grid.push_back({center, numObjects, weightsSum});
}
return grid;
}
std::vector<AbstractedGridspace> getFarAbstractedGrid(const Eigen::Vector2f& position, float radius, GridspacePositionCalculationMethod calcMethod)
{
std::vector<AbstractedGridspace> farGrid;
for (const auto& [cell, objects] : objects_) {
Eigen::Vector2f center = computeCellCenter(cell, calcMethod);
// Check if the cell is outside the radius from the position
if ((center - position).norm() > radius) {
int numObjects = static_cast<int>(objects.size());
farGrid.push_back({center, numObjects});
}
}
return farGrid;
}
std::vector<int> getObjectsInCell(const Eigen::Vector2f& position) const
{
int cell = findCell(position);
std::unordered_set<int> nearbyObjects;
std::unordered_set<int> objectsInCell = objects_.find(cell)->second;
nearbyObjects.insert(objectsInCell.begin(), objectsInCell.end());
return std::vector<int>(nearbyObjects.begin(), nearbyObjects.end());
}
void update(Eigen::Vector2f* positions, int numPositions)
{
clear();
for (int i = 0; i < numPositions; ++i)
{
add(i, positions[i]);
}
positions_ = positions;
}
void clear()
{
objects_.clear();
occupiedCells_.clear();
}
float getCellSize() const
{
return cellSize_;
}
private:
std::unordered_map<int, std::unordered_set<int>> objects_; // maps cell IDs to sets of object indices
std::unordered_map<int, std::unordered_set<int>> occupiedCells_; // maps object indices to sets of cell IDs
Eigen::Vector2f* positions_;
int numPositions_;
float cellSize_;
std::unordered_set<int> findCells(const Eigen::Vector2f& position, float radius = 0.0f) const
{
// Compute the range of cells that overlap with the circle defined by the radius and position
float cellRadius = radius + cellSize_ / 2;
int left = static_cast<int>(std::floor((position.x() - cellRadius) / cellSize_));
int right = static_cast<int>(std::floor((position.x() + cellRadius) / cellSize_));
int top = static_cast<int>(std::floor((position.y() - cellRadius) / cellSize_));
int bottom = static_cast<int>(std::floor((position.y() + cellRadius) / cellSize_));
// Add all cells within the range to the set of occupied cells
std::unordered_set<int> cells;
for (int x = left; x <= right; ++x) {
for (int y = top; y <= bottom; ++y) {
int cell = x + y * (1 << 16); // combine x and y into a single integer key
cells.insert(cell);
}
}
return cells;
}
int findCell(const Eigen::Vector2f& position) const
{
int x = static_cast<int>(std::floor(position.x() / cellSize_));
int y = static_cast<int>(std::floor(position.y() / cellSize_));
return x + y * (1 << 16); // combine x and y into a single integer key
}
Eigen::Vector2f computeCellCenter(int cell, GridspacePositionCalculationMethod calcMethod, float* weights = nullptr) const
{
if(calcMethod == GridspacePositionCalculationMethod::AVERAGE)
{
// Compute the average position of all objects in the cell
Eigen::Vector2f center(0.0f, 0.0f);
std::unordered_set<int> objects = objects_.find(cell)->second;
for (int objectIndex : objects)
{
center += positions_[objectIndex];
}
return center / static_cast<float>(objects.size());
}
else if(calcMethod == GridspacePositionCalculationMethod::WEIGHTED_AVERAGE)
{
// Compute the weighted average position of all objects in the cell
Eigen::Vector2f center(0.0f, 0.0f);
float weightsSum = 0.0f;
for (int objectIndex : objects_.find(cell)->second)
{
center += positions_[objectIndex] * weights[objectIndex];
weightsSum += weights[objectIndex];
}
return center / weightsSum;
}
else if(calcMethod == GridspacePositionCalculationMethod::CENTER)
{
// Compute the center position of the cell given its unique ID
int x = cell & 0xFFFF;
int y = (cell >> 16) & 0xFFFF;
return Eigen::Vector2f((x + 0.5f) * cellSize_, (y + 0.5f) * cellSize_);
}
return Eigen::Vector2f(0.0f, 0.0f);
}
void add(int objectIndex, const Eigen::Vector2f& position)
{
// Compute the set of cells that the object occupies
std::unordered_set<int> cells = findCells(position);
// Add the object index to the set of objects for each occupied cell
for (int cell : cells) {
objects_[cell].insert(objectIndex);
}
// Store the set of cells that the object occupies
occupiedCells_[objectIndex] = std::move(cells);
}
void remove(int objectIndex)
{
// Remove the object index from the set of objects for each occupied cell
for (int cell : occupiedCells_[objectIndex]) {
objects_[cell].erase(objectIndex);
}
// Remove the object's occupied cells from the map
occupiedCells_.erase(objectIndex);
}
};

View File

@ -1,357 +0,0 @@
import graphViewJS from "assets/graph-view.txt.js";
import graphWASMJS from "assets/graph-wasm.txt.js";
import renderWorkerJS from "assets/graph-render-worker.txt.js";
import graphWASM from "assets/graph-wasm.wasm";
import websiteJS from "assets/website.txt.js";
import webpageStyles from "assets/plugin-styles.txt.css";
import deferredJS from "assets/deferred.txt.js";
import deferredCSS from "assets/deferred.txt.css";
import themeLoadJS from "assets/theme-load.txt.js";
import tinyColorJS from "assets/tinycolor.txt.js";
import pixiJS from "assets/pixi.txt.js";
import minisearchJS from "assets/minisearch.txt.js";
import { Path } from "scripts/utils/path.js";
import { Asset, AssetType, InlinePolicy, LoadMethod, Mutability } from "./assets/asset.js";
import { ObsidianStyles } from "./assets/obsidian-styles.js";
import { OtherPluginStyles } from "./assets/other-plugin-styles.js";
import { ThemeStyles } from "./assets/theme-styles.js";
import { SnippetStyles } from "./assets/snippet-styles.js";
import { MathjaxStyles } from "./assets/mathjax-styles.js";
import { CustomHeadContent } from "./assets/custom-head-content.js";
import { Settings, SettingsPage } from "scripts/settings/settings.js";
import { GlobalVariableStyles } from "./assets/global-variable-styles.js";
import { Favicon } from "./assets/favicon.js";
import { FetchBuffer } from "./assets/local-fetch-buffer.js";
import { ExportLog } from "./render-log.js";
import { SupportedPluginStyles } from "./assets/supported-plugin-styles.js";
import { fileTypeFromBuffer } from "file-type";
import { MarkdownWebpageRendererAPIOptions } from "scripts/api-options.js";
const mime = require('mime');
export class AssetHandler
{
public static vaultPluginsPath: Path;
public static staticAssets: Asset[] = [];
public static dynamicAssets: Asset[] = [];
public static allAssets: Asset[] = [];
public static temporaryAssets: Asset[] = [];
// this path is used to generate the relative path to the images folder, likewise for the other paths
private static libraryFolder: Path;
private static mediaFolder: Path;
private static jsFolder: Path;
private static cssFolder: Path;
private static fontFolder: Path;
private static htmlFolder: Path;
public static exportOptions: MarkdownWebpageRendererAPIOptions = new MarkdownWebpageRendererAPIOptions();
public static get libraryPath(): Path
{
if (!this.libraryFolder) this.initialize();
return AssetHandler.libraryFolder.copy.makeWebStyle(this.exportOptions.webStylePaths);
}
public static get mediaPath(): Path
{
if (!this.mediaFolder) this.initialize();
return AssetHandler.mediaFolder.copy.makeWebStyle(this.exportOptions.webStylePaths);
}
public static get jsPath(): Path
{
if (!this.jsFolder) this.initialize();
return AssetHandler.jsFolder.copy.makeWebStyle(this.exportOptions.webStylePaths);
}
public static get cssPath(): Path
{
if (!this.cssFolder) this.initialize();
return AssetHandler.cssFolder.copy.makeWebStyle(this.exportOptions.webStylePaths);
}
public static get fontPath(): Path
{
if (!this.fontFolder) this.initialize();
return AssetHandler.fontFolder.copy.makeWebStyle(this.exportOptions.webStylePaths);
}
public static get htmlPath(): Path
{
if (!this.htmlFolder) this.initialize();
return AssetHandler.htmlFolder.copy.makeWebStyle(this.exportOptions.webStylePaths);
}
// styles
public static obsidianStyles: ObsidianStyles = new ObsidianStyles();
public static otherPluginStyles: OtherPluginStyles = new OtherPluginStyles();
public static themeStyles: ThemeStyles = new ThemeStyles();
public static snippetStyles: SnippetStyles = new SnippetStyles();
public static mathjaxStyles: MathjaxStyles = new MathjaxStyles();
public static globalDataStyles: GlobalVariableStyles = new GlobalVariableStyles();
public static supportedPluginStyles: SupportedPluginStyles = new SupportedPluginStyles();
public static websiteStyles: Asset = new Asset("main-styles.css", webpageStyles, AssetType.Style, InlinePolicy.AutoHead, true, Mutability.Static, LoadMethod.Async, 4);
public static deferredCSS: Asset = new Asset("deferred.css", deferredCSS, AssetType.Style, InlinePolicy.InlineHead, true, Mutability.Static, LoadMethod.Defer);
// scripts
public static websiteJS: Asset = new Asset("webpage.js", websiteJS, AssetType.Script, InlinePolicy.AutoHead, true, Mutability.Static, LoadMethod.Async);
public static graphViewJS: Asset = new Asset("graph-view.js", graphViewJS, AssetType.Script, InlinePolicy.AutoHead, true, Mutability.Static);
public static graphWASMJS: Asset = new Asset("graph-wasm.js", graphWASMJS, AssetType.Script, InlinePolicy.AutoHead, true, Mutability.Static);
public static graphWASM: Asset = new Asset("graph-wasm.wasm", Buffer.from(graphWASM), AssetType.Script, InlinePolicy.Download, false, Mutability.Static);
public static renderWorkerJS: Asset = new Asset("graph-render-worker.js", renderWorkerJS, AssetType.Script, InlinePolicy.AutoHead, true, Mutability.Static);
public static deferredJS: Asset = new Asset("deferred.js", deferredJS, AssetType.Script, InlinePolicy.InlineHead, true, Mutability.Static, LoadMethod.Defer);
public static themeLoadJS: Asset = new Asset("theme-load.js", themeLoadJS, AssetType.Script, InlinePolicy.Inline, true, Mutability.Static, LoadMethod.Defer);
public static tinyColorJS: Asset = new Asset("tinycolor.js", tinyColorJS, AssetType.Script, InlinePolicy.AutoHead, true, Mutability.Static);
public static pixiJS: Asset = new Asset("pixi.js", pixiJS, AssetType.Script, InlinePolicy.AutoHead, true, Mutability.Static, LoadMethod.Async, 100, "https://cdnjs.cloudflare.com/ajax/libs/pixi.js/7.4.0/pixi.min.js");
public static minisearchJS: Asset = new Asset("minisearch.js", minisearchJS, AssetType.Script, InlinePolicy.AutoHead, true, Mutability.Static, LoadMethod.Async, 100, "https://cdn.jsdelivr.net/npm/minisearch@6.3.0/dist/umd/index.min.js");
// other
public static favicon: Favicon = new Favicon();
public static externalLinkIcon: Asset;
public static customHeadContent: CustomHeadContent = new CustomHeadContent();
public static mainJsModTime: number = 0;
public static async initialize()
{
this.libraryFolder = new Path("lib").makeUnixStyle();
this.mediaFolder = this.libraryFolder.joinString("media").makeUnixStyle();
this.jsFolder = this.libraryFolder.joinString("scripts").makeUnixStyle();
this.cssFolder = this.libraryFolder.joinString("styles").makeUnixStyle();
this.fontFolder = this.libraryFolder.joinString("fonts").makeUnixStyle();
this.htmlFolder = this.libraryFolder.joinString("html").makeUnixStyle();
this.vaultPluginsPath = Path.vaultPath.joinString(app.vault.configDir, "plugins/").makeAbsolute();
// by default all static assets have a modified time the same as main.js
this.mainJsModTime = this.vaultPluginsPath.joinString("webpage-html-export/main.js").stat?.mtimeMs ?? 0;
this.staticAssets.forEach(asset => asset.modifiedTime = this.mainJsModTime);
this.allAssets.sort((a, b) => a.loadPriority - b.loadPriority);
let loadPromises = []
for (let asset of this.allAssets)
{
loadPromises.push(asset.load(this.exportOptions));
}
await Promise.all(loadPromises);
let graphViewJSPath = this.graphViewJS.getAssetPath();
this.graphViewJS.getHTML = () => `<script type="module" async id="graph-view-script" src="${graphViewJSPath}"></script>`;
}
public static async reloadAssets()
{
// remove all temporary assets from allAssets
this.allAssets = this.allAssets.filter(asset => asset.mutability != Mutability.Temporary);
this.temporaryAssets = [];
let i = 0;
let loadPromises = []
for (let asset of this.dynamicAssets)
{
let loadPromise = asset.load(this.exportOptions);
loadPromise.then(() =>
{
i++;
ExportLog.progress(i, this.dynamicAssets.length, "Initialize Export", "Loading asset: " + asset.filename, "var(--color-yellow)");
});
loadPromises.push(loadPromise);
}
await Promise.all(loadPromises);
}
public static getAssetsOfType(type: AssetType): Asset[]
{
let assets = this.allAssets.filter(asset => asset.type == type);
assets = assets.concat(this.allAssets.map(asset => asset.childAssets).flat().filter(asset => asset.type == type));
return assets;
}
public static getAssetsOfInlinePolicy(inlinePolicy: InlinePolicy): Asset[]
{
let assets = this.allAssets.filter(asset => asset.inlinePolicy == inlinePolicy);
assets = assets.concat(this.allAssets.map(asset => asset.childAssets).flat().filter(asset => asset.inlinePolicy == inlinePolicy));
return assets;
}
private static filterDownloads(downloads: Asset[], options: MarkdownWebpageRendererAPIOptions): Asset[]
{
if (!options.addGraphView || !options.addSidebars)
{
downloads = downloads.filter(asset => ![this.graphViewJS, this.graphWASMJS, this.graphWASM, this.renderWorkerJS, this.tinyColorJS, this.pixiJS].includes(asset));
}
if (!options.addSearch || !options.addSidebars)
{
downloads = downloads.filter(asset => ![this.minisearchJS].includes(asset));
}
if (!options.includeCSS)
{
downloads = downloads.filter(asset => asset.type != AssetType.Style);
}
if (!options.includeJS)
{
downloads = downloads.filter(asset => asset.type != AssetType.Script);
}
// remove duplicates
downloads = downloads.filter((asset, index, self) => self.findIndex((t) => t.relativePath.asString == asset.relativePath.asString) === index);
// remove assets with no content
downloads = downloads.filter(asset => asset.content && asset.content.length > 0);
return downloads;
}
public static getDownloads(options: MarkdownWebpageRendererAPIOptions): Asset[]
{
let downloads = this.getAssetsOfInlinePolicy(InlinePolicy.Download)
.concat(this.getAssetsOfInlinePolicy(InlinePolicy.DownloadHead));
if (!options.inlineMedia)
{
downloads = downloads.concat(this.getAssetsOfInlinePolicy(InlinePolicy.Auto));
downloads = downloads.concat(this.getAssetsOfInlinePolicy(InlinePolicy.AutoHead));
}
downloads = this.filterDownloads(downloads, options);
downloads.sort((a, b) => b.loadPriority - a.loadPriority);
return downloads;
}
public static getHeadReferences(options: MarkdownWebpageRendererAPIOptions): string
{
let head = "";
let referenceAssets = this.getAssetsOfInlinePolicy(InlinePolicy.DownloadHead)
.concat(this.getAssetsOfInlinePolicy(InlinePolicy.AutoHead))
.concat(this.getAssetsOfInlinePolicy(InlinePolicy.InlineHead));
referenceAssets = this.filterDownloads(referenceAssets, options);
referenceAssets.sort((a, b) => b.loadPriority - a.loadPriority);
for (let asset of referenceAssets)
{
head += asset.getHTML(options);
}
return head;
}
/*Takes a style sheet string and creates assets from every font or image url embedded in it*/
public static async getStyleChildAssets(asset: Asset, makeBase64External: boolean = false): Promise<string>
{
if (typeof asset.content != "string") throw new Error("Asset content is not a string");
let content = asset.content.replaceAll("app://obsidian.md/", "");
let urls = Array.from(content.matchAll(/url\("([^"]+)"\)|url\('([^']+)'\)/g));
// remove duplicates
urls = urls.filter((url, index, self) => self.findIndex((t) => t[0] === url[0]) === index);
// use this mutability for child assets
let promises = [];
for (let urlObj of urls)
{
let url = urlObj[1] || urlObj[2];
url = url.trim();
// we don't need to download online assets if we are not making the page offline compatible
if (!this.exportOptions.offlineResources && url.startsWith("http")) continue;
if (url == "") continue;
if (url.startsWith("data:"))
{
if (!this.exportOptions.inlineMedia && makeBase64External)
{
// decode the base64 data and create an Asset from it
// then replace the url with the relative path to the asset
function hash(str:string, seed = 0) // taken from https://stackoverflow.com/questions/7616461/generate-a-hash-from-string-in-javascript
{
let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed;
for(let i = 0, ch; i < str.length; i++) {
ch = str.charCodeAt(i);
h1 = Math.imul(h1 ^ ch, 2654435761);
h2 = Math.imul(h2 ^ ch, 1597334677);
}
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);
h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);
h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);
return 4294967296 * (2097151 & h2) + (h1 >>> 0);
}
let splitData = url.split(",")
let data = splitData.slice(1).join(",");
let extension = Asset.mimeToExtention(splitData[0].split(":")[1].split(";")[0]);
let buffer = Buffer.from(data, "base64");
let dataHash = hash(data);
let filename = `${dataHash}.${extension}`;
if (extension == '')
{
let type = await fileTypeFromBuffer(buffer);
if (type) extension = type.ext;
filename = `${dataHash}.${extension}`;
}
let type = Asset.extentionToType(extension);
let childAsset = new Asset(filename, buffer, type, InlinePolicy.Download, false, Mutability.Child);
asset.childAssets.push(childAsset);
let loadPromise = childAsset.load(this.exportOptions);
promises.push(loadPromise);
loadPromise.then(() =>
{
if (childAsset.content == undefined || childAsset.content == null || childAsset.content.length == 0)
{
return;
}
let newPath = childAsset.getAssetPath(asset.getAssetPath());
content = content.replaceAll(url, newPath.asString);
});
}
continue;
}
let path = new Path(url);
let type = Asset.extentionToType(path.extension);
let childAsset = new FetchBuffer(path.fullName, url, type, InlinePolicy.Download, false, Mutability.Child);
asset.childAssets.push(childAsset);
let loadPromise = childAsset.load(this.exportOptions);
promises.push(loadPromise);
loadPromise.then(() =>
{
if (childAsset.content == undefined || childAsset.content == null || childAsset.content.length == 0)
{
return;
}
if (this.exportOptions.inlineMedia)
{
let base64 = childAsset.content.toString("base64");
content = content.replaceAll(url, `data:${mime.getType(url)};base64,${base64}`);
}
else
{
childAsset.relativeDirectory.makeWebStyle(this.exportOptions.webStylePaths);
if (this.exportOptions.webStylePaths) childAsset.filename = Path.toWebStyle(childAsset.filename);
let newPath = childAsset.getAssetPath(asset.getAssetPath());
content = content.replaceAll(url, newPath.asString);
}
});
}
await Promise.all(promises);
return content;
}
}

View File

@ -1,320 +0,0 @@
import { Path } from "scripts/utils/path";
import { Downloadable } from "scripts/utils/downloadable";
import { ExportLog } from "../render-log";
import { Settings, SettingsPage } from "scripts/settings/settings";
import { AssetHandler } from "../asset-handler";
import { MarkdownWebpageRendererAPIOptions } from "scripts/api-options";
const { minify: runMinify } = require('html-minifier-terser');
const mime = require('mime');
export enum AssetType
{
Style = "style", // css
Script = "script", // js
Media = "media", // images, videos, audio, etc
HTML = "html", // reusable html
Font = "font", // fonts
Other = "other" // anything else
}
export enum InlinePolicy
{
AutoHead = "autohead", // Fine for js, css, html, and fonts. Include data itself or reference to downloaded data into head.
Auto = "auto", // Does not auto insert anywhere, but chooses format on whether assets are being inlined. (will download if not inlined)
Inline = "inline", // Does not auto insert anywhere, but is always inline format
Download = "download", // Just download, does not auto insert anywhere
DownloadHead = "downloadhead", // Download and include ref in head
InlineHead = "inlinehead", // Inline raw data into head
None = "none" // Do nothing with this asset
}
export enum Mutability
{
Static = "static", // this asset never changes
Dynamic = "dynamic", // this asset can change
Temporary = "temporary", // this asset is created only for the current export and is deleted afterwards
Child = "child", // this asset will only change when the parent changes
}
export enum LoadMethod
{
Default = "",
Async = "async",
Defer = "defer"
}
export class Asset extends Downloadable
{
public type: AssetType; // what type of asset is this
public inlinePolicy: InlinePolicy; // should this asset be inlined into the html file
public mutability: Mutability; // can this asset change
public minify: boolean; // should the asset be minified
public loadMethod: LoadMethod = LoadMethod.Default; // should this asset be loaded asynchronously if possible
public loadPriority: number = 100; // the priority of this asset when loading
public onlineURL: string | undefined = undefined; // the link to the asset online
public childAssets: Asset[] = []; // assets that depend on this asset
public exportOptions: MarkdownWebpageRendererAPIOptions;
constructor(filename: string, content: string | Buffer, type: AssetType, inlinePolicy: InlinePolicy, minify: boolean, mutability: Mutability, loadMethod: LoadMethod = LoadMethod.Async, loadPriority: number = 100, cdnPath: string | undefined = undefined, options: MarkdownWebpageRendererAPIOptions = new MarkdownWebpageRendererAPIOptions())
{
if(options.webStylePaths) filename = Path.toWebStyle(filename);
super(filename, content, Asset.typeToPath(type));
this.exportOptions = options;
this.type = type;
this.inlinePolicy = inlinePolicy;
this.mutability = mutability;
this.minify = minify;
this.loadMethod = loadMethod;
this.loadPriority = loadPriority;
this.onlineURL = cdnPath;
this.modifiedTime = Date.now();
switch(mutability)
{
case Mutability.Static:
AssetHandler.staticAssets.push(this);
this.modifiedTime = AssetHandler.mainJsModTime; // by default all static assets have a modified time the same as main.js
break;
case Mutability.Dynamic:
AssetHandler.dynamicAssets.push(this);
break;
case Mutability.Temporary:
AssetHandler.temporaryAssets.push(this);
break;
}
if (mutability != Mutability.Child) AssetHandler.allAssets.push(this);
}
public async load(options: MarkdownWebpageRendererAPIOptions): Promise<void>
{
this.exportOptions = options;
if (this.type == AssetType.Style && typeof this.content == "string")
{
this.childAssets = [];
this.content = await AssetHandler.getStyleChildAssets(this, false);
}
if (this.minify)
{
this.minifyAsset();
}
}
override async download(downloadDirectory: Path): Promise<void>
{
if (this.isInlineFormat(this.exportOptions)) return;
await super.download(downloadDirectory);
}
public static typeToPath(type: AssetType): Path
{
switch(type)
{
case AssetType.Style:
return AssetHandler.cssPath;
case AssetType.Script:
return AssetHandler.jsPath;
case AssetType.Media:
return AssetHandler.mediaPath;
case AssetType.HTML:
return AssetHandler.htmlPath;
case AssetType.Font:
return AssetHandler.fontPath;
case AssetType.Other:
return AssetHandler.libraryPath;
}
}
public static extentionToType(extention: string): AssetType
{
const mediaTypes = ["png", "jpg", "jpeg", "gif", "svg", "webp", "ico", "mp4", "webm", "ogg", "mp3", "wav", "flac", "aac", "m4a", "opus"];
const scriptTypes = ["js", "ts"];
const styleTypes = ["css", "scss", "sass", "less"];
const htmlTypes = ["html", "htm"];
const fontTypes = ["ttf", "woff", "woff2", "eot", "otf"];
extention = extention.toLowerCase().replace(".", "");
if (mediaTypes.includes(extention)) return AssetType.Media;
if (scriptTypes.includes(extention)) return AssetType.Script;
if (styleTypes.includes(extention)) return AssetType.Style;
if (htmlTypes.includes(extention)) return AssetType.HTML;
if (fontTypes.includes(extention)) return AssetType.Font;
return AssetType.Other;
}
/**Minify the contents of a JS or CSS string (No HTML)*/
async minifyAsset()
{
if (this.type == AssetType.HTML || typeof this.content != "string") return;
let isJS = this.type == AssetType.Script;
let isCSS = this.type == AssetType.Style;
let tempContent = this.content;
try
{
// add script or style tags so that minifier can minify it as html
if (isJS) tempContent = `<script>${tempContent}</script>`;
if (isCSS) tempContent = `<style>${tempContent}</style>`;
tempContent = await runMinify(tempContent, { minifyCSS: isCSS, minifyJS: isJS, removeComments: true, collapseWhitespace: true});
// remove the <script> or <style> tags
tempContent = tempContent.replace("<script>", "").replace("</script>", "").replace("<style>", "").replace("</style>", "");
this.content = tempContent;
}
catch (e)
{
ExportLog.warning("Unable to minify " + (isJS ? "JS" : "CSS") + " file.");
// remove whitespace manually
this.content = this.content.replace(/[\n\r]+/g, "");
}
}
public getAssetPath(relativeFrom: Path | undefined = undefined): Path
{
if (this.isInlineFormat(this.exportOptions)) return new Path("");
if (relativeFrom == undefined) relativeFrom = Path.rootPath;
let toRoot = Path.getRelativePath(relativeFrom, Path.rootPath);
let newPath = toRoot.join(this.relativePath).makeUnixStyle();
newPath.makeWebStyle(this.exportOptions.webStylePaths);
return newPath;
}
protected isInlineFormat(options: MarkdownWebpageRendererAPIOptions): boolean
{
let isInlineFormat = this.inlinePolicy == InlinePolicy.Inline ||
this.inlinePolicy == InlinePolicy.InlineHead ||
((this.inlinePolicy == InlinePolicy.Auto || this.inlinePolicy == InlinePolicy.AutoHead) &&
(
(options.inlineCSS! && this.type == AssetType.Style) ||
(options.inlineJS! && this.type == AssetType.Script) ||
(options.inlineMedia! && this.type == AssetType.Media) ||
(options.inlineHTML! && this.type == AssetType.HTML) ||
(options.inlineFonts! && this.type == AssetType.Font)
));
return isInlineFormat;
}
protected isRefFormat(options: MarkdownWebpageRendererAPIOptions): boolean
{
let isRefFormat = this.inlinePolicy == InlinePolicy.Download ||
this.inlinePolicy == InlinePolicy.DownloadHead ||
((this.inlinePolicy == InlinePolicy.Auto || this.inlinePolicy == InlinePolicy.AutoHead) &&
!(
(options.inlineCSS! && this.type == AssetType.Style) ||
(options.inlineJS! && this.type == AssetType.Script) ||
(options.inlineMedia! && this.type == AssetType.Media) ||
(options.inlineHTML! && this.type == AssetType.HTML) ||
(options.inlineFonts! && this.type == AssetType.Font)
));
return isRefFormat;
}
public getHTML(options: MarkdownWebpageRendererAPIOptions): string
{
if(this.isInlineFormat(options))
{
switch(this.type)
{
case AssetType.Style:
return `<style>${this.content}</style>`;
case AssetType.Script:
return `<script ${this.loadMethod}>${this.content}</script>`;
case AssetType.Media:
return `<${this.getHTMLTagName()} src="${this.getContentBase64()}"/>`;
case AssetType.HTML:
return this.content as string;
case AssetType.Font:
return `<style>@font-face{font-family:'${this.filename}';src:url(${this.getContentBase64()}) format('woff2');}</style>`;
default:
return "";
}
}
if (this.isRefFormat(options))
{
let path = this.getAssetPath(undefined).asString;
if (options.offlineResources === false && this.onlineURL) path = this.onlineURL;
let include = "";
let attr = "";
switch(this.type)
{
case AssetType.Style:
include = `<link rel="stylesheet" href="${path}">`;
if (this.loadMethod == LoadMethod.Async)
{
include = `<link rel="preload" href="${path}" as="style" onload="this.onload=null;this.rel='stylesheet'"><noscript><link rel="stylesheet" href="${path}"></noscript>`
}
return include;
case AssetType.Script:
include = `<script ${this.loadMethod} id="${this.relativePath.basename + "-script"}" src="${path}" onload='this.onload=null;this.setAttribute(\"loaded\", \"true\")'></script>`;
return include;
case AssetType.Media:
attr = this.loadMethod == LoadMethod.Defer ? "loading='eager'" : "loading='lazy'";
if (this.loadMethod == LoadMethod.Default) attr = "";
include = `<${this.getHTMLTagName()} src="${path}" ${attr} />`;
return include;
case AssetType.Font:
include = `<style>@font-face{font-family:'${this.filename}';src:url('${path}') format('woff2');}</style>`;
return include;
case AssetType.HTML:
return `<include src="${path}"></include>`
default:
return "";
}
}
return "";
}
public static mimeToExtention(mime: string): string
{
const FileMimeType: {[key: string]: string} = {'audio/x-mpeg': 'mpega', 'application/postscript': 'ps', 'audio/x-aiff': 'aiff', 'application/x-aim': 'aim', 'image/x-jg': 'art', 'video/x-ms-asf': 'asx', 'audio/basic': 'ulw', 'video/x-msvideo': 'avi', 'video/x-rad-screenplay': 'avx', 'application/x-bcpio': 'bcpio', 'application/octet-stream': 'exe', 'image/bmp': 'dib', 'text/html': 'html', 'application/x-cdf': 'cdf', 'application/pkix-cert': 'cer', 'application/java': 'class', 'application/x-cpio': 'cpio', 'application/x-csh': 'csh', 'text/css': 'css', 'application/msword': 'doc', 'application/xml-dtd': 'dtd', 'video/x-dv': 'dv', 'application/x-dvi': 'dvi', 'application/vnd.ms-fontobject': 'eot', 'text/x-setext': 'etx', 'image/gif': 'gif', 'application/x-gtar': 'gtar', 'application/x-gzip': 'gz', 'application/x-hdf': 'hdf', 'application/mac-binhex40': 'hqx', 'text/x-component': 'htc', 'image/ief': 'ief', 'text/vnd.sun.j2me.app-descriptor': 'jad', 'application/java-archive': 'jar', 'text/x-java-source': 'java', 'application/x-java-jnlp-file': 'jnlp', 'image/jpeg': 'jpg', 'application/javascript': 'js', 'text/plain': 'txt', 'application/json': 'json', 'audio/midi': 'midi', 'application/x-latex': 'latex', 'audio/x-mpegurl': 'm3u', 'image/x-macpaint': 'pnt', 'text/troff': 'tr', 'application/mathml+xml': 'mathml', 'application/x-mif': 'mif', 'video/quicktime': 'qt', 'video/x-sgi-movie': 'movie', 'audio/mpeg': 'mpa', 'video/mp4': 'mp4', 'video/mpeg': 'mpg', 'video/mpeg2': 'mpv2', 'application/x-wais-source': 'src', 'application/x-netcdf': 'nc', 'application/oda': 'oda', 'application/vnd.oasis.opendocument.database': 'odb', 'application/vnd.oasis.opendocument.chart': 'odc', 'application/vnd.oasis.opendocument.formula': 'odf', 'application/vnd.oasis.opendocument.graphics': 'odg', 'application/vnd.oasis.opendocument.image': 'odi', 'application/vnd.oasis.opendocument.text-master': 'odm', 'application/vnd.oasis.opendocument.presentation': 'odp', 'application/vnd.oasis.opendocument.spreadsheet': 'ods', 'application/vnd.oasis.opendocument.text': 'odt', 'application/vnd.oasis.opendocument.graphics-template': 'otg', 'application/vnd.oasis.opendocument.text-web': 'oth', 'application/vnd.oasis.opendocument.presentation-template': 'otp', 'application/vnd.oasis.opendocument.spreadsheet-template': 'ots', 'application/vnd.oasis.opendocument.text-template': 'ott', 'application/ogg': 'ogx', 'video/ogg': 'ogv', 'audio/ogg': 'spx', 'application/x-font-opentype': 'otf', 'audio/flac': 'flac', 'application/annodex': 'anx', 'audio/annodex': 'axa', 'video/annodex': 'axv', 'application/xspf+xml': 'xspf', 'image/x-portable-bitmap': 'pbm', 'image/pict': 'pict', 'application/pdf': 'pdf', 'image/x-portable-graymap': 'pgm', 'audio/x-scpls': 'pls', 'image/png': 'png', 'image/x-portable-anymap': 'pnm', 'image/x-portable-pixmap': 'ppm', 'application/vnd.ms-powerpoint': 'pps', 'image/vnd.adobe.photoshop': 'psd', 'image/x-quicktime': 'qtif', 'image/x-cmu-raster': 'ras', 'application/rdf+xml': 'rdf', 'image/x-rgb': 'rgb', 'application/vnd.rn-realmedia': 'rm', 'application/rtf': 'rtf', 'text/richtext': 'rtx', 'application/font-sfnt': 'sfnt', 'application/x-sh': 'sh', 'application/x-shar': 'shar', 'application/x-stuffit': 'sit', 'application/x-sv4cpio': 'sv4cpio', 'application/x-sv4crc': 'sv4crc', 'image/svg+xml': 'svg', 'application/x-shockwave-flash': 'swf', 'application/x-tar': 'tar', 'application/x-tcl': 'tcl', 'application/x-tex': 'tex', 'application/x-texinfo': 'texinfo', 'image/tiff': 'tiff', 'text/tab-separated-values': 'tsv', 'application/x-font-ttf': 'ttf', 'application/x-ustar': 'ustar', 'application/voicexml+xml': 'vxml', 'image/x-xbitmap': 'xbm', 'application/xhtml+xml': 'xhtml', 'application/vnd.ms-excel': 'xls', 'application/xml': 'xsl', 'image/x-xpixmap': 'xpm', 'application/xslt+xml': 'xslt', 'application/vnd.mozilla.xul+xml': 'xul', 'image/x-xwindowdump': 'xwd', 'application/vnd.visio': 'vsd', 'audio/x-wav': 'wav', 'image/vnd.wap.wbmp': 'wbmp', 'text/vnd.wap.wml': 'wml', 'application/vnd.wap.wmlc': 'wmlc', 'text/vnd.wap.wmlsc': 'wmls', 'application/vnd.wap.wmlscriptc': 'wmlscriptc', 'video/x-ms-wmv': 'wmv', 'application/font-woff': 'woff', 'application/font-woff2': 'woff2', 'model/vrml': 'wrl', 'application/wspolicy+xml': 'wspolicy', 'application/x-compress': 'z', 'application/zip': 'zip'};
return FileMimeType[mime] || mime.split("/")[1] || "txt";
}
public static extentionToMime(extention: string): string
{
if (extention.startsWith(".")) extention = extention.slice(1);
const FileMimeType: {[key: string]: string} = {'mpega': 'audio/x-mpeg', 'ps': 'application/postscript', 'aiff': 'audio/x-aiff', 'aim': 'application/x-aim', 'art': 'image/x-jg', 'asx': 'video/x-ms-asf', 'ulw': 'audio/basic', 'avi': 'video/x-msvideo', 'avx': 'video/x-rad-screenplay', 'bcpio': 'application/x-bcpio', 'exe': 'application/octet-stream', 'dib': 'image/bmp', 'html': 'text/html', 'cdf': 'application/x-cdf', 'cer': 'application/pkix-cert', 'class': 'application/java', 'cpio': 'application/x-cpio', 'csh': 'application/x-csh', 'css': 'text/css', 'doc': 'application/msword', 'dtd': 'application/xml-dtd', 'dv': 'video/x-dv', 'dvi': 'application/x-dvi', 'eot': 'application/vnd.ms-fontobject', 'etx': 'text/x-setext', 'gif': 'image/gif', 'gtar': 'application/x-gtar', 'gz': 'application/x-gzip', 'hdf': 'application/x-hdf', 'hqx': 'application/mac-binhex40', 'htc': 'text/x-component', 'ief': 'image/ief', 'jad': 'text/vnd.sun.j2me.app-descriptor', 'jar': 'application/java-archive', 'java': 'text/x-java-source', 'jnlp': 'application/x-java-jnlp-file', 'jpg': 'image/jpeg', 'js': 'application/javascript', 'txt': 'text/plain', 'json': 'application/json', 'midi': 'audio/midi', 'latex': 'application/x-latex', 'm3u': 'audio/x-mpegurl', 'pnt': 'image/x-macpaint', 'tr': 'text/troff', 'mathml': 'application/mathml+xml', 'mif': 'application/x-mif', 'qt': 'video/quicktime', 'movie': 'video/x-sgi-movie', 'mpa': 'audio/mpeg', 'mp4': 'video/mp4', 'mpg': 'video/mpeg', 'mpv2': 'video/mpeg2', 'src': 'application/x-wais-source', 'nc': 'application/x-netcdf', 'oda': 'application/oda', 'odb': 'application/vnd.oasis.opendocument.database', 'odc': 'application/vnd.oasis.opendocument.chart', 'odf': 'application/vnd.oasis.opendocument.formula', 'odg': 'application/vnd.oasis.opendocument.graphics', 'odi': 'application/vnd.oasis.opendocument.image', 'odm': 'application/vnd.oasis.opendocument.text-master', 'odp': 'application/vnd.oasis.opendocument.presentation', 'ods': 'application/vnd.oasis.opendocument.spreadsheet', 'odt': 'application/vnd.oasis.opendocument.text', 'otg': 'application/vnd.oasis.opendocument.graphics-template', 'oth': 'application/vnd.oasis.opendocument.text-web', 'otp': 'application/vnd.oasis.opendocument.presentation-template', 'ots': 'application/vnd.oasis.opendocument.spreadsheet-template', 'ott': 'application/vnd.oasis.opendocument.text-template', 'ogx': 'application/ogg', 'ogv': 'video/ogg', 'spx': 'audio/ogg', 'otf': 'application/x-font-opentype', 'flac': 'audio/flac', 'anx': 'application/annodex', 'axa': 'audio/annodex', 'axv': 'video/annodex', 'xspf': 'application/xspf+xml', 'pbm': 'image/x-portable-bitmap', 'pict': 'image/pict', 'pdf': 'application/pdf', 'pgm': 'image/x-portable-graymap', 'pls': 'audio/x-scpls', 'png': 'image/png', 'pnm': 'image/x-portable-anymap', 'ppm': 'image/x-portable-pixmap', 'pps': 'application/vnd.ms-powerpoint', 'psd': 'image/vnd.adobe.photoshop', 'qtif': 'image/x-quicktime', 'ras': 'image/x-cmu-raster', 'rdf': 'application/rdf+xml', 'rgb': 'image/x-rgb', 'rm': 'application/vnd.rn-realmedia', 'rtf': 'application/rtf', 'rtx': 'text/richtext', 'sfnt': 'application/font-sfnt', 'sh': 'application/x-sh', 'shar': 'application/x-shar', 'sit': 'application/x-stuffit', 'sv4cpio': 'application/x-sv4cpio', 'sv4crc': 'application/x-sv4crc', 'svg': 'image/svg+xml', 'swf': 'application/x-shockwave-flash', 'tar': 'application/x-tar', 'tcl': 'application/x-tcl', 'tex': 'application/x-tex', 'texinfo': 'application/x-texinfo', 'tiff': 'image/tiff', 'tsv': 'text/tab-separated-values', 'ttf': 'application/x-font-ttf', 'ustar': 'application/x-ustar', 'vxml': 'application/voicexml+xml', 'xbm': 'image/x-xbitmap', 'xhtml': 'application/xhtml+xml', 'xls': 'application/vnd.ms-excel', 'xsl': 'application/xml', 'xpm': 'image/x-xpixmap', 'xslt': 'application/xslt+xml', 'xul': 'application/vnd.mozilla.xul+xml', 'xwd': 'image/x-xwindowdump', 'vsd': 'application/vnd.visio', 'wav': 'audio/x-wav', 'wbmp': 'image/vnd.wap.wbmp', 'wml': 'text/vnd.wap.wml', 'wmlc': 'application/vnd.wap.wmlc', 'wmls': 'text/vnd.wap.wmlsc', 'wmlscriptc': 'application/vnd.wap.wmlscriptc', 'wmv': 'video/x-ms-wmv', 'woff': 'application/font-woff', 'woff2': 'application/font-woff2', 'wrl': 'model/vrml', 'wspolicy': 'application/wspolicy+xml', 'z': 'application/x-compress', 'zip': 'application/zip'};
return FileMimeType[extention] || "application/octet-stream";
}
public getContentBase64(): string
{
let extension = this.filename.split(".").pop() || "txt";
let mimeType = mime(extension) || "text/plain";
let base64 = this.content.toString("base64");
return `data:${mimeType};base64,${base64}`;
}
private getHTMLTagName(): string
{
switch(this.type)
{
case AssetType.Style:
return "link";
case AssetType.Script:
return "script";
case AssetType.Font:
return "style";
case AssetType.HTML:
return "include";
}
// media
let extension = this.filename.split(".").pop() || "txt";
const extToTag: {[key: string]: string} = {"png": "img", "jpg": "img", "jpeg": "img", "tiff": "img", "bmp": "img", "avif": "img", "apng": "img", "gif": "img", "svg": "img", "webp": "img", "ico": "img", "mp4": "video", "webm": "video", "ogg": "video", "3gp": "video", "mov": "video", "mpeg": "video", "mp3": "audio", "wav": "audio", "flac": "audio", "aac": "audio", "m4a": "audio", "opus": "audio"};
return extToTag[extension] || "img";
}
}

View File

@ -1,47 +0,0 @@
import { Asset, AssetType, InlinePolicy, Mutability } from "./asset";
import { Path } from "scripts/utils/path";
import { Settings, SettingsPage } from "scripts/settings/settings";
import { ExportLog } from "../render-log";
import { MarkdownWebpageRendererAPIOptions } from "scripts/api-options";
export class CustomHeadContent extends Asset
{
public content: string = "";
constructor()
{
super("custom-head-content.html", "", AssetType.HTML, InlinePolicy.AutoHead, false, Mutability.Dynamic);
}
override async load(options: MarkdownWebpageRendererAPIOptions)
{
if (!SettingsPage.loaded) return;
let customHeadPath = new Path(Settings.customHeadContentPath);
if (customHeadPath.isEmpty)
{
this.content = "";
return;
}
let validation = customHeadPath.validate(
{
allowEmpty: false,
allowFiles: true,
allowAbsolute: true,
allowRelative: true,
requireExists: true
});
if (!validation.valid)
{
this.content = "";
ExportLog.error(validation.error + customHeadPath.asString);
return;
}
this.modifiedTime = customHeadPath.stat?.mtimeMs ?? this.modifiedTime;
this.content = await customHeadPath.readFileString() ?? "";
await super.load(options);
}
}

View File

@ -1,43 +0,0 @@
import { Asset, AssetType, InlinePolicy, Mutability } from "./asset";
import { Settings, SettingsPage } from "scripts/settings/settings";
import { Path } from "scripts/utils/path";
import defaultIcon from "assets/icon.png";
import { MarkdownWebpageRendererAPIOptions } from "scripts/api-options";
export class Favicon extends Asset
{
public content: Buffer;
constructor()
{
super("favicon.png", "", AssetType.Media, InlinePolicy.AutoHead, false, Mutability.Dynamic);
}
override async load(options: MarkdownWebpageRendererAPIOptions)
{
if (Settings.faviconPath == "") this.content = Buffer.from(defaultIcon);
let iconPath = new Path(Settings.faviconPath);
let icon = await iconPath.readFileBuffer();
if (icon)
{
this.content = icon;
this.filename = "favicon" + iconPath.extension;
this.modifiedTime = iconPath.stat?.mtimeMs ?? this.modifiedTime;
}
await super.load(options);
}
public override getHTML(): string
{
if (Settings.inlineAssets)
{
return `<link rel="icon" href="data:image/png;base64,${this.content.toString("base64")}">`;
}
else
{
return `<link rel="icon" href="${this.getAssetPath()}">`;
}
}
}

View File

@ -1,43 +0,0 @@
import { MarkdownWebpageRendererAPIOptions } from "scripts/api-options";
import { Asset, AssetType, InlinePolicy, LoadMethod, Mutability } from "./asset";
import { Settings, SettingsPage } from "scripts/settings/settings";
export class GlobalVariableStyles extends Asset
{
public content: string = "";
constructor()
{
super("global-variable-styles.css", "", AssetType.Style, InlinePolicy.AutoHead, true, Mutability.Dynamic, LoadMethod.Async, 6);
}
override async load(options: MarkdownWebpageRendererAPIOptions)
{
let bodyStyle = (document.body.getAttribute("style") ?? "").replaceAll("\"", "'").replaceAll("; ", " !important;\n\t");
let lineWidth = Settings.documentWidth || "40em";
let sidebarWidth = Settings.sidebarWidth || "20em";
if (!isNaN(Number(lineWidth))) lineWidth += "px";
if (!isNaN(Number(sidebarWidth))) sidebarWidth += "px";
let lineWidthCss = `min(${lineWidth}, calc(100vw - 2em))`;
this.content =
`
:root body
{
--line-width: ${lineWidthCss};
--line-width-adaptive: ${lineWidthCss};
--file-line-width: ${lineWidthCss};
--sidebar-width: min(${sidebarWidth}, 80vw);
}
body
{
${bodyStyle}
}
`
this.modifiedTime = Date.now();
await super.load(options);
}
}

View File

@ -1,106 +0,0 @@
import { Asset, AssetType, InlinePolicy, LoadMethod, Mutability } from "./asset";
import { Path } from "scripts/utils/path";
import { ExportLog } from "../render-log";
import { RequestUrlResponse, requestUrl } from "obsidian";
import { Utils } from "scripts/utils/utils";
import { fileTypeFromBuffer } from "file-type";
import { MarkdownWebpageRendererAPIOptions } from "scripts/api-options";
export class FetchBuffer extends Asset
{
public content: Buffer;
public url: Path | string;
constructor(filename: string, url: Path | string, type: AssetType, inlinePolicy: InlinePolicy, minify: boolean, mutability: Mutability, loadPriority?: number)
{
super(filename, "", type, inlinePolicy, minify, mutability, LoadMethod.Default, loadPriority);
this.url = url;
let stringURL = this.url instanceof Path ? this.url.asString : this.url;
if (stringURL.startsWith("http")) this.onlineURL = stringURL;
}
override async load(options: MarkdownWebpageRendererAPIOptions)
{
if (this.url instanceof Path)
{
if (this.url.isRelative)
{
this.url.setWorkingDirectory("").makeAbsolute();
}
this.url = this.url.makeUnixStyle().asString;
}
if (options.offlineResources === false && this.url.startsWith("http")) return;
if (this.url.startsWith("http") && (this.url.split(".").length <= 2 || this.url.split("/").length <= 2))
{
this.onlineURL = undefined;
return;
}
let res: RequestUrlResponse | Response;
try
{
if (this.url.startsWith("http"))
{
// first ping with a fetch "no-cors" request to see if the server is available
let testResp = await Utils.urlAvailable(this.url);
if (testResp.type == "opaque")
res = await requestUrl(this.url);
else
{
ExportLog.log(`Url ${this.url} is not available`);
return;
}
}
else
{
// local file
res = await fetch(this.url);
}
}
catch (e)
{
ExportLog.log(e, `Failed to fetch ${this.url}`);
return;
}
if (res.status != 200)
{
ExportLog.log(`Failed to fetch ${this.url} with status ${res.status}`);
return;
}
let data;
if (res instanceof Response)
{
data = await res.arrayBuffer();
}
else
{
data = res.arrayBuffer;
}
this.content = Buffer.from(data);
this.modifiedTime = Date.now();
if (this.relativePath.extension == '')
{
let type = await fileTypeFromBuffer(this.content);
if (type)
{
this.relativePath.setExtension(type.ext);
this.filename = this.relativePath.fullName;
this.type = Asset.extentionToType(type.ext);
this.relativeDirectory = Asset.typeToPath(this.type);
}
}
await super.load(options);
}
}

View File

@ -1,44 +0,0 @@
import { MarkdownWebpageRendererAPIOptions } from "scripts/api-options";
import { Asset, AssetType, InlinePolicy, Mutability } from "./asset";
export class MathjaxStyles extends Asset
{
private mathjaxStylesheet: CSSStyleSheet | undefined = undefined;
private lastMathjaxChanged: number = -1;
public content: string = "";
constructor()
{
super("mathjax.css", "", AssetType.Style, InlinePolicy.Inline, true, Mutability.Dynamic);
}
override async load(options: MarkdownWebpageRendererAPIOptions)
{
// @ts-ignore
if (this.mathjaxStylesheet == undefined) this.mathjaxStylesheet = Array.from(document.styleSheets).find((sheet) => sheet.ownerNode.id == ("MJX-CHTML-styles"));
if (this.mathjaxStylesheet == undefined)
{
return;
}
this.modifiedTime = Date.now();
// @ts-ignore
let changed = this.mathjaxStylesheet?.ownerNode.getAttribute("data-change");
if (changed != this.lastMathjaxChanged)
{
this.content = "";
for (let i = 0; i < this.mathjaxStylesheet.cssRules.length; i++)
{
this.content += this.mathjaxStylesheet.cssRules[i].cssText + "\n";
}
}
else
{
return;
}
this.lastMathjaxChanged = changed;
await super.load(options);
}
}

View File

@ -1,106 +0,0 @@
import { Asset, AssetType, InlinePolicy, LoadMethod, Mutability } from "./asset";
import { SettingsPage } from "scripts/settings/settings";
import { ExportLog } from "../render-log";
import obsidianStyleOverrides from "assets/obsidian-styles.txt.css";
import { MarkdownWebpageRendererAPIOptions } from "scripts/api-options";
export class ObsidianStyles extends Asset
{
public content: string = "";
constructor()
{
super("obsidian.css", "", AssetType.Style, InlinePolicy.AutoHead, true, Mutability.Dynamic, LoadMethod.Default, 10);
}
public static stylesFilter =
["workspace-", "cm-", "ghost", "leaf", "CodeMirror",
"@media", "pdf", "xfa", "annotation", "@keyframes",
"load", "@-webkit", "setting", "filter", "decorator",
"dictionary", "status", "windows", "titlebar", "source",
"menu", "message", "popover", "suggestion", "prompt",
"tab", "HyperMD", "workspace", "publish",
"backlink", "sync", "vault", "mobile", "tablet", "phone",
"textLayer", "header", "linux", "macos", "rename", "edit",
"progress", "native", "aria", "tooltip",
"drop", "sidebar", "mod-windows", "is-frameless",
"is-hidden-frameless", "obsidian-app", "show-view-header",
"is-maximized", "is-translucent", "community"];
public static stylesKeep = ["scrollbar", "input[type", "table", "markdown-rendered", "css-settings-manager", "inline-embed", "background", "token"];
removeSelectors(css: string, containing: string): string
{
let regex = new RegExp(`([\w :*+~\\-\\.\\>\\[\\]()"=]*${containing}[\\w\\s:*+~\\-\\.\\>\\[\\]()"=]+)(,|{)`, "gm");
let toRemove = [...css.matchAll(regex)];
for (let match of toRemove)
{
css = css.replace(match[1], "");
}
css = css.trim();
return css;
}
override async load(options: MarkdownWebpageRendererAPIOptions)
{
this.content = "";
let appSheet = document.styleSheets[1];
let stylesheets = document.styleSheets;
for (let i = 0; i < stylesheets.length; i++)
{
if (stylesheets[i].href && stylesheets[i].href?.includes("app.css"))
{
appSheet = stylesheets[i];
break;
}
}
this.content += obsidianStyleOverrides;
for (let i = 0; i < appSheet.cssRules.length; i++)
{
let rule = appSheet.cssRules[i];
if (rule)
{
let skip = false;
let cssText = rule.cssText;
let selector = cssText.split("{")[0];
for (let keep of ObsidianStyles.stylesKeep)
{
if (!selector.includes(keep))
{
// filter out certain unused styles to reduce file size
for (let filter of ObsidianStyles.stylesFilter)
{
if (selector.includes(filter))
{
skip = true;
break;
}
}
}
else
{
skip = false;
break;
}
}
if (skip) continue;
cssText = this.removeSelectors(cssText, "\\.cm-");
if(cssText.startsWith("{")) continue; // skip empty rules
cssText += "\n";
this.content += cssText;
}
}
this.modifiedTime = Date.now();
await super.load(options);
}
}

View File

@ -1,41 +0,0 @@
import { Settings, SettingsPage } from "scripts/settings/settings";
import { Asset, AssetType, InlinePolicy, LoadMethod, Mutability } from "./asset";
import { AssetHandler } from "../asset-handler";
import { MarkdownWebpageRendererAPIOptions } from "scripts/api-options";
export class OtherPluginStyles extends Asset
{
public content: string = "";
private lastEnabledPluginStyles: string = "";
constructor()
{
super("other-plugins.css", "", AssetType.Style, InlinePolicy.AutoHead, true, Mutability.Dynamic, LoadMethod.Async, 9);
}
override async load(options: MarkdownWebpageRendererAPIOptions)
{
if(this.lastEnabledPluginStyles == Settings.includePluginCSS) return;
this.content = "";
let thirdPartyPluginStyleNames = Settings.includePluginCSS.split("\n");
for (let i = 0; i < thirdPartyPluginStyleNames.length; i++)
{
if (!thirdPartyPluginStyleNames[i] || (thirdPartyPluginStyleNames[i] && !(/\S/.test(thirdPartyPluginStyleNames[i])))) continue;
let path = AssetHandler.vaultPluginsPath.joinString(thirdPartyPluginStyleNames[i].replace("\n", ""), "styles.css");
if (!path.exists) continue;
let style = await path.readFileString();
if (style)
{
this.content += style;
console.log("Loaded plugin style: " + thirdPartyPluginStyleNames[i] + " size: " + style.length);
}
}
this.modifiedTime = Date.now();
this.lastEnabledPluginStyles = Settings.includePluginCSS;
await super.load(options);
}
}

View File

@ -1,51 +0,0 @@
import { Asset, AssetType, InlinePolicy, LoadMethod, Mutability } from "./asset";
import { Path } from "scripts/utils/path";
import { ExportLog } from "../render-log";
import { MarkdownWebpageRendererAPIOptions } from "scripts/api-options";
export class SnippetStyles extends Asset
{
public content: string = "";
constructor()
{
super("snippets.css", "", AssetType.Style, InlinePolicy.AutoHead, true, Mutability.Dynamic, LoadMethod.Async, 2);
}
private static getEnabledSnippets(): string[]
{
/*@ts-ignore*/
return app.vault.config?.enabledCssSnippets ?? [];
}
private static async getStyleSnippetsContent(): Promise<string[]>
{
let snippetContents : string[] = [];
let enabledSnippets = this.getEnabledSnippets();
for (let i = 0; i < enabledSnippets.length; i++)
{
let path = new Path(`.obsidian/snippets/${enabledSnippets[i]}.css`).absolute();
if (path.exists) snippetContents.push(await path.readFileString() ?? "\n");
}
return snippetContents;
}
override async load(options: MarkdownWebpageRendererAPIOptions)
{
let snippetsList = await SnippetStyles.getStyleSnippetsContent();
let snippets = "\n";
for (let i = 0; i < snippetsList.length; i++)
{
snippets += snippetsList[i] + "\n";
}
// replace "publish" styles with a high specificity prefix
snippets = snippets.replaceAll(/^publish /gm, "html body[class].publish ");
this.content = snippets;
this.modifiedTime = Date.now();
await super.load(options);
}
}

View File

@ -1,85 +0,0 @@
import { Settings, SettingsPage } from "scripts/settings/settings";
import { Asset, AssetType, InlinePolicy, LoadMethod, Mutability } from "./asset";
import { ExportLog } from "../render-log";
import { MarkdownWebpageRendererAPIOptions } from "scripts/api-options";
export class SupportedPluginStyles extends Asset
{
public content: string = "";
constructor()
{
super("supported-plugins.css", "", AssetType.Style, InlinePolicy.AutoHead, true, Mutability.Dynamic, LoadMethod.Async, 5);
}
override async load(options: MarkdownWebpageRendererAPIOptions)
{
this.content = "";
let stylesheets = document.styleSheets;
for(let i = 1; i < stylesheets.length; i++)
{
// @ts-ignore
let styleID = stylesheets[i].ownerNode?.id;
if
(
styleID == "ADMONITIONS_CUSTOM_STYLE_SHEET" ||
styleID == "css-settings-manager" ||
(Settings.includeSvelteCSS && this.isSvelteStylesheet(stylesheets[i]))
)
{
ExportLog.log("Including stylesheet: " + styleID);
let style = stylesheets[i].cssRules;
for(let item in style)
{
if(style[item].cssText != undefined)
{
this.content += "\n" + style[item].cssText;
}
}
}
this.content += "\n\n /* ---- */\n\n";
}
this.modifiedTime = Date.now();
await super.load(options);
}
getStylesheetContent(stylesheet: CSSStyleSheet): string
{
let content = "";
let style = stylesheet.cssRules;
for(let item in style)
{
if(style[item].cssText != undefined)
{
content += "\n" + style[item].cssText;
}
}
return content;
}
isSvelteStylesheet(stylesheet: CSSStyleSheet): boolean
{
if(stylesheet.ownerNode == undefined) return false;
// @ts-ignore
let styleID = stylesheet.ownerNode.id;
if (styleID.contains("svelte")) return true;
let sheetContent = this.getStylesheetContent(stylesheet);
let first1000 = sheetContent.substring(0, 1000);
if (first1000.contains(".svelte-"))
{
return true;
}
return false;
}
}

View File

@ -1,53 +0,0 @@
import { Asset, AssetType, InlinePolicy, LoadMethod, Mutability } from "./asset";
import { Path } from "scripts/utils/path";
import { ExportLog } from "../render-log";
import { AssetHandler } from "../asset-handler";
import { MarkdownWebpageRendererAPIOptions } from "scripts/api-options";
export class ThemeStyles extends Asset
{
public content: string = "";
private lastThemeName: string = "";
constructor()
{
super("theme.css", "", AssetType.Style, InlinePolicy.AutoHead, true, Mutability.Dynamic, LoadMethod.Default, 8);
}
private static async getThemeContent(themeName: string): Promise<string>
{
if (themeName == "Default") return "/* Using default theme. */";
let themePath = AssetHandler.vaultPluginsPath.joinString(`../themes/${themeName}/theme.css`).absolute();
if (!themePath.exists)
{
ExportLog.warning("Cannot find theme at path: \n\n" + themePath);
return "";
}
let themeContent = await themePath.readFileString() ?? "";
return themeContent;
}
private static getCurrentThemeName(): string
{
/*@ts-ignore*/
let themeName = app.vault.config?.cssTheme;
return (themeName ?? "") == "" ? "Default" : themeName;
}
override async load(options: MarkdownWebpageRendererAPIOptions)
{
let themeName = ThemeStyles.getCurrentThemeName();
if (themeName == this.lastThemeName)
{
this.modifiedTime = 0;
return;
}
this.content = await ThemeStyles.getThemeContent(themeName);
this.modifiedTime = Date.now();
this.lastThemeName = themeName;
await super.load(options);
}
}

View File

@ -1,166 +0,0 @@
import { EmojiStyle, Settings, SettingsPage } from "scripts/settings/settings";
import { AssetHandler } from "./asset-handler";
import { AssetType } from "./assets/asset";
import { ExportLog } from "./render-log";
import { getIcon as getObsidianIcon, requestUrl } from "obsidian";
import { Utils } from "scripts/utils/utils";
import { ObsidianStyles } from "./assets/obsidian-styles";
export namespace HTMLGeneration
{
export function createThemeToggle(container: HTMLElement) : HTMLElement
{
let label = container.createEl("label");
let input = label.createEl("input");
let div = label.createDiv();
label.classList.add("theme-toggle-container");
label.setAttribute("for", "theme_toggle");
input.classList.add("theme-toggle-input");
input.setAttribute("type", "checkbox");
input.setAttribute("id", "theme_toggle");
div.classList.add("toggle-background");
return label;
}
let _validBodyClasses: string | undefined = undefined;
export async function getValidBodyClasses(cleanCache: boolean): Promise<string>
{
if (cleanCache) _validBodyClasses = undefined;
if (_validBodyClasses) return _validBodyClasses;
let bodyClasses = Array.from(document.body.classList);
// filter classes
bodyClasses = bodyClasses.filter((value) =>
ObsidianStyles.stylesKeep.some(keep => value.includes(keep)) ||
!ObsidianStyles.stylesFilter.some(filter => value.includes(filter))
);
let validClasses = "";
validClasses += " publish ";
validClasses += " css-settings-manager ";
// keep body classes that are referenced in the styles
let styles = AssetHandler.getAssetsOfType(AssetType.Style);
let i = 0;
let classes: string[] = [];
for (var style of styles)
{
ExportLog.progress(i, styles.length, "Compiling css classes", "Scanning: " + style.filename, "var(--color-yellow)");
if (typeof(style.content) != "string") continue;
// this matches every class name with the dot
let matches = Array.from(style.content.matchAll(/\.([A-Za-z_-]+[\w-]+)/g));
let styleClasses = matches.map(match => match[0].substring(1).trim());
// remove duplicates
styleClasses = styleClasses.filter((value, index, self) => self.indexOf(value) === index);
classes = classes.concat(styleClasses);
i++;
await Utils.delay(0);
}
// remove duplicates
ExportLog.progress(1, 1, "Filtering classes", "...", "var(--color-yellow)");
classes = classes.filter((value, index, self) => self.indexOf(value) === index);
ExportLog.progress(1, 1, "Sorting classes", "...", "var(--color-yellow)");
classes = classes.sort();
i = 0;
for (var bodyClass of bodyClasses)
{
ExportLog.progress(i, bodyClasses.length, "Collecting valid classes", "Scanning: " + bodyClass, "var(--color-yellow)");
if (classes.includes(bodyClass))
{
validClasses += bodyClass + " ";
}
i++;
}
ExportLog.progress(1, 1, "Cleanup classes", "...", "var(--color-yellow)");
_validBodyClasses = validClasses.replace(/\s\s+/g, ' ');
// convert to array and remove duplicates
ExportLog.progress(1, 1, "Filter duplicate classes", _validBodyClasses.length + " classes", "var(--color-yellow)");
_validBodyClasses = _validBodyClasses.split(" ").filter((value, index, self) => self.indexOf(value) === index).join(" ").trim();
ExportLog.progress(1, 1, "Classes done", "...", "var(--color-yellow)");
return _validBodyClasses;
}
export function getLucideIcon(iconName: string): string | undefined
{
const iconEl = getObsidianIcon(iconName);
if (iconEl)
{
let svg = iconEl.outerHTML;
iconEl.remove();
return svg;
}
else
{
return undefined;
}
}
export function getEmojiIcon(iconCode: string): string | undefined
{
let iconCodeInt = parseInt(iconCode, 16);
if (!isNaN(iconCodeInt))
{
return String.fromCodePoint(iconCodeInt);
}
else
{
return undefined;
}
}
export async function getIcon(iconName: string): Promise<string>
{
if (iconName.startsWith('emoji//'))
{
const iconCode = iconName.replace(/^emoji\/\//, '');
iconName = getEmojiIcon(iconCode) ?? "<22>";
}
else if (iconName.startsWith('lucide//'))
{
const lucideIconName = iconName.replace(/^lucide\/\//, '');
iconName = getLucideIcon(lucideIconName) ?? "<22>";
}
// if it's an emoji convert it into a twemoji
if ((/^\p{Emoji}/gu).test(iconName))
{
let codepoint = [...iconName].map(e => e.codePointAt(0)!.toString(16)).join(`-`);
switch (Settings.emojiStyle)
{
case EmojiStyle.Twemoji:
return `<img src="https://cdnjs.cloudflare.com/ajax/libs/twemoji/14.0.2/svg/${codepoint}.svg" class="emoji" />`;
case EmojiStyle.OpenMoji:
console.log(codepoint);
return `<img src="https://openmoji.org/data/color/svg/${codepoint.toUpperCase()}.svg" class="emoji" />`;
case EmojiStyle.OpenMojiOutline:
let req = await requestUrl(`https://openmoji.org/data/black/svg/${codepoint.toUpperCase()}.svg`);
if (req.status == 200)
return req.text.replaceAll(/#00+/g, "currentColor").replaceAll(`stroke-width="2"`, `stroke-width="5"`);
return iconName;
case EmojiStyle.FluentUI:
return `<img src="https://emoji.fluent-cdn.com/1.0.0/100x100/${codepoint}.png" class="emoji" />`;
default:
return iconName;
}
}
return getLucideIcon(iconName.toLowerCase()) ?? iconName; // try and parse a plain lucide icon name
}
}

View File

@ -1,142 +0,0 @@
import { Path } from "scripts/utils/path";
import { _MarkdownRendererInternal } from "scripts/render-api";
import { Settings, SettingsPage } from "scripts/settings/settings";
export namespace ExportLog
{
export let fullLog: string = "";
function logToString(message: any, title: string)
{
let messageString = (typeof message === "string") ? message : JSON.stringify(message).replaceAll("\n", "\n\t\t");
let titleString = title != "" ? title + "\t" : "";
let log = `${titleString}${messageString}\n`;
return log;
}
function humanReadableJSON(object: any)
{
let string = JSON.stringify(object, null, 2).replaceAll(/\"|\{|\}|,/g, "").split("\n").map((s) => s.trim()).join("\n\t");
// make the properties into a table
let lines = string.split("\n");
lines = lines.filter((line) => line.contains(":"));
let names = lines.map((line) => line.split(":")[0] + " ");
let values = lines.map((line) => line.split(":").slice(1).join(":"));
let maxLength = Math.max(...names.map((name) => name.length)) + 3;
let table = "";
for (let i = 0; i < names.length; i++)
{
let padString = i % 2 == 0 ? "-" : " ";
table += `${names[i].padEnd(maxLength, padString)}${values[i]}\n`;
}
return table;
}
export function log(message: any, messageTitle: string = "")
{
pullPathLogs();
messageTitle = `[INFO] ${messageTitle}`
fullLog += logToString(message, messageTitle);
if(SettingsPage.loaded && !(Settings.logLevel == "all")) return;
if (messageTitle != "") console.log(messageTitle + " ", message);
else console.log(message);
_MarkdownRendererInternal._reportInfo(messageTitle, message);
}
export function warning(message: any, messageTitle: string = "")
{
pullPathLogs();
messageTitle = `[WARNING] ${messageTitle}`
fullLog += logToString(message, messageTitle);
if(SettingsPage.loaded && !["warning", "all"].contains(Settings.logLevel)) return;
if (messageTitle != "") console.warn(messageTitle + " ", message);
else console.warn(message);
_MarkdownRendererInternal._reportWarning(messageTitle, message);
}
export function error(message: any, messageTitle: string = "", fatal: boolean = false)
{
pullPathLogs();
messageTitle = (fatal ? "[FATAL ERROR] " : "[ERROR] ") + messageTitle;
fullLog += logToString(message, messageTitle);
if (SettingsPage.loaded && !fatal && !["error", "warning", "all"].contains(Settings.logLevel)) return;
if (fatal && messageTitle == "Error") messageTitle = "Fatal Error";
if (messageTitle != "") console.error(messageTitle + " ", message);
else console.error(message);
_MarkdownRendererInternal._reportError(messageTitle, message, fatal);
}
export function progress(complete: number, total:number, message: string, subMessage: string, progressColor: string = "var(--interactive-accent)")
{
pullPathLogs();
if (total == 0)
{
complete = 1;
total = 1;
}
_MarkdownRendererInternal._reportProgress(complete, total, message, subMessage, progressColor);
}
function pullPathLogs()
{
let logs = Path.dequeueLog();
for (let thisLog of logs)
{
switch (thisLog.type)
{
case "info":
log(thisLog.message, thisLog.title);
break;
case "warn":
warning(thisLog.message, thisLog.title);
break;
case "error":
error(thisLog.message, thisLog.title, false);
break;
case "fatal":
error(thisLog.message, thisLog.title, true);
break;
}
}
}
export function getDebugInfo()
{
let debugInfo = "";
debugInfo += `Log:\n${fullLog}\n\n`;
let settingsCopy = Object.assign({}, Settings);
//@ts-ignore
settingsCopy.filesToExport = settingsCopy.filesToExport[0].length;
settingsCopy.includePluginCSS = settingsCopy.includePluginCSS.split("\n").length + " plugins included";
debugInfo += `Settings:\n${humanReadableJSON(settingsCopy)}\n\n`;
// @ts-ignore
let loadedPlugins = Object.values(app.plugins.plugins).filter((plugin) => plugin._loaded == true).map((plugin) => plugin.manifest.name).join("\n\t");
debugInfo += `Enabled Plugins:\n\t${loadedPlugins}`;
return debugInfo;
}
export function testThrowError(chance: number)
{
if (Math.random() < chance)
{
throw new Error("Test error");
}
}
}

View File

@ -1,64 +0,0 @@
from http.server import HTTPServer, BaseHTTPRequestHandler
import json
import webbrowser
import mimetypes
import base64
database_string = open('C:/Main Documents/Obsidian/Export/database.json','r', encoding='utf-8').read()
website = json.loads(database_string) # dictionary relating paths to file data
html_404 = """
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="/lib/styles/obsidian-styles.css">
<link rel="stylesheet" href="/lib/styles/theme.css">
</head>
<body style="background-color: var(--background-primary);">
<h1 style="position: absolute; top: 50%; left: 50%; translate: -50%, -50%;">404 Not Found</h1>
</body>
<html>
"""
class Serve(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == '/':
self.path = 'index.html'
if not self.path in website:
# return first html file in website
self.path = next(p for p in list(website.keys()) if p.endswith(".html"))
print("index.html does not exist. Using", self.path, "instead.")
if(self.path.startswith("/")):
self.path = self.path[1:]
ext = "." + self.path.split(".")[-1]
if self.path in website:
data = website[self.path]
self.send_response(200)
self.send_header('Content-type', mimetypes.types_map[ext])
self.end_headers()
if ext == ".html" or ext == ".css" or ext == ".js":
response = bytes(data,'utf-8')
self.wfile.write(response)
else:
response = base64.decodebytes(bytes(data, 'utf-8'))
self.wfile.write(response)
else:
self.send_response(404)
self.end_headers()
self.wfile.write(bytes(html_404,'utf-8'))
httpd = HTTPServer(('localhost',8080),Serve)
# open http://localhost:8080/ in the browser
webbrowser.open('http://localhost:8080/')
httpd.serve_forever()

View File

@ -1,43 +0,0 @@
# -*- mode: python ; coding: utf-8 -*-
a = Analysis(
['server.py'],
pathex=[],
binaries=[],
datas=[],
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
)
pyz = PYZ(a.pure)
exe = EXE(
pyz,
a.scripts,
[],
exclude_binaries=True,
name='server',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=True,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)
coll = COLLECT(
exe,
a.binaries,
a.datas,
strip=False,
upx=True,
upx_exclude=[],
name='server',
)

View File

@ -1,149 +0,0 @@
// imports from obsidian API
import { Notice, Plugin, TFile, TFolder, requestUrl} from 'obsidian';
// modules that are part of the plugin
import { AssetHandler } from './html-generation/asset-handler';
import { Settings, SettingsPage } from './settings/settings';
import { HTMLExporter } from './exporter';
import { Path } from './utils/path';
import { ExportLog } from './html-generation/render-log';
import { ExportModal } from './settings/export-modal';
import { MarkdownRendererAPI } from './render-api';
export default class HTMLExportPlugin extends Plugin
{
static plugin: HTMLExportPlugin;
static updateInfo: {updateAvailable: boolean, latestVersion: string, currentVersion: string, updateNote: string} = {updateAvailable: false, latestVersion: "0", currentVersion: "0", updateNote: ""};
static pluginVersion: string = "0.0.0";
public api = MarkdownRendererAPI;
public settings = Settings;
public assetHandler = AssetHandler;
public Path = Path;
public async exportDocker() {
await HTMLExporter.export(true, undefined, new Path("/output"));
}
async onload()
{
console.log("Loading webpage-html-export plugin");
HTMLExportPlugin.plugin = this;
this.checkForUpdates();
HTMLExportPlugin.pluginVersion = this.manifest.version;
// @ts-ignore
window.WebpageHTMLExport = this;
this.addSettingTab(new SettingsPage(this));
await SettingsPage.loadSettings();
await AssetHandler.initialize();
this.addRibbonIcon("folder-up", "Export Vault to HTML", () =>
{
HTMLExporter.export(false);
});
// register callback for file rename so we can update the saved files to export
this.registerEvent(this.app.vault.on("rename", SettingsPage.renameFile));
this.addCommand({
id: 'export-html-vault',
name: 'Export using previous settings',
callback: () =>
{
HTMLExporter.export(true);
}
});
this.addCommand({
id: 'export-html-current',
name: 'Export only current file using previous settings',
callback: () =>
{
let file = this.app.workspace.getActiveFile();
if (!file)
{
new Notice("No file is currently open!", 5000);
return;
}
HTMLExporter.export(true, [file]);
}
});
this.addCommand({
id: 'export-html-setting',
name: 'Set html export settings',
callback: () =>
{
HTMLExporter.export(false);
}
});
this.registerEvent(
this.app.workspace.on("file-menu", (menu, file) =>
{
menu.addItem((item) =>
{
item
.setTitle("Export as HTML")
.setIcon("download")
.setSection("export")
.onClick(() =>
{
ExportModal.title = `Export ${file.name} as HTML`;
if(file instanceof TFile)
{
HTMLExporter.export(false, [file]);
}
else if(file instanceof TFolder)
{
let filesInFolder = this.app.vault.getFiles().filter((f) => new Path(f.path).directory.asString.startsWith(file.path));
HTMLExporter.export(false, filesInFolder);
}
else
{
ExportLog.error("File is not a TFile or TFolder! Invalid type: " + typeof file + "");
new Notice("File is not a File or Folder! Invalid type: " + typeof file + "", 5000);
}
});
});
})
);
}
async checkForUpdates(): Promise<{updateAvailable: boolean, latestVersion: string, currentVersion: string, updateNote: string}>
{
let currentVersion = this.manifest.version;
try
{
let url = "https://raw.githubusercontent.com/KosmosisDire/obsidian-webpage-export/master/manifest.json?cache=" + Date.now() + "";
if (this.manifest.version.endsWith("b")) url = "https://raw.githubusercontent.com/KosmosisDire/obsidian-webpage-export/master/manifest-beta.json?cache=" + Date.now() + "";
let manifestResp = await requestUrl(url);
if (manifestResp.status != 200) throw new Error("Could not fetch manifest");
let manifest = manifestResp.json;
let latestVersion = manifest.version ?? currentVersion;
let updateAvailable = currentVersion < latestVersion;
let updateNote = manifest.updateNote ?? "";
HTMLExportPlugin.updateInfo = {updateAvailable: updateAvailable, latestVersion: latestVersion, currentVersion: currentVersion, updateNote: updateNote};
if(updateAvailable) ExportLog.log("Update available: " + latestVersion + " (current: " + currentVersion + ")");
return HTMLExportPlugin.updateInfo;
}
catch
{
ExportLog.log("Could not check for update");
HTMLExportPlugin.updateInfo = {updateAvailable: false, latestVersion: currentVersion, currentVersion: currentVersion, updateNote: ""};
return HTMLExportPlugin.updateInfo;
}
}
onunload()
{
ExportLog.log('unloading webpage-html-export plugin');
}
}

View File

@ -1,296 +0,0 @@
import { TAbstractFile, TFile, TFolder } from "obsidian";
import { FileTree, FileTreeItem } from "./file-tree";
import { Path } from "scripts/utils/path";
import { Website } from "./website";
import { MarkdownRendererAPI } from "scripts/render-api";
export class FilePickerTree extends FileTree
{
public children: FilePickerTreeItem[] = [];
public selectAllItem: FilePickerTreeItem | undefined;
public constructor(files: TFile[], keepOriginalExtensions: boolean = false, sort = true)
{
super(files, keepOriginalExtensions, sort);
}
protected override async populateTree(): Promise<void>
{
for (let file of this.files)
{
let pathSections: TAbstractFile[] = [];
let parentFile: TAbstractFile = file;
while (parentFile != undefined)
{
pathSections.push(parentFile);
// @ts-ignore
parentFile = parentFile.parent;
}
pathSections.reverse();
let parent: FilePickerTreeItem | FilePickerTree = this;
for (let i = 1; i < pathSections.length; i++)
{
let section = pathSections[i];
let isFolder = section instanceof TFolder;
// make sure this section hasn't already been added
let child = parent.children.find(sibling => sibling.title == section.name && sibling.isFolder == isFolder && sibling.depth == i) as FilePickerTreeItem | undefined;
if (child == undefined)
{
child = new FilePickerTreeItem(this, parent, i);
child.title = section.name;
child.isFolder = isFolder;
if(child.isFolder)
{
child.href = section.path;
child.itemClass = "mod-tree-folder"
}
else
{
child.file = file;
child.itemClass = "mod-tree-file"
}
parent.children.push(child);
}
parent = child;
}
if (parent instanceof FilePickerTreeItem)
{
let titleInfo = await Website.getTitleAndIcon(file, true);
let path = new Path(file.path).makeUnixStyle();
if (file instanceof TFolder) path.makeForceFolder();
else
{
parent.originalExtension = path.extensionName;
if(!this.keepOriginalExtensions && MarkdownRendererAPI.isConvertable(path.extensionName)) path.setExtension("html");
}
parent.href = path.asString;
parent.title = path.basename == "." ? "" : titleInfo.title;
}
}
if (this.sort)
{
this.sortAlphabetically();
this.sortByIsFolder();
}
}
public async generateTree(container: HTMLElement): Promise<void>
{
await super.generateTree(container);
// add a select all button at the top
let selectAllButton = new FilePickerTreeItem(this, this, 0);
selectAllButton.title = "Select All";
selectAllButton.itemClass = "mod-tree-control";
let itemEl = await selectAllButton.generateItemHTML(container);
// remove all event listeners from the select all button
let oldItemEl = itemEl;
itemEl = itemEl.cloneNode(true) as HTMLDivElement;
selectAllButton.checkbox = itemEl.querySelector("input") as HTMLInputElement;
selectAllButton.itemEl = itemEl;
selectAllButton.childContainer = itemEl.querySelector(".tree-item-children") as HTMLDivElement;
container.prepend(itemEl);
oldItemEl.remove();
let localThis = this;
function selectAll()
{
let checked = selectAllButton.checkbox.checked;
selectAllButton.check(!checked);
localThis.forAllChildren((child) => child.check(!checked));
}
selectAllButton.checkbox.addEventListener("click", (event) =>
{
selectAllButton.checkbox.checked = !selectAllButton.checkbox.checked;
selectAll();
event.stopPropagation();
});
selectAllButton.itemEl.addEventListener("click", () =>
{
selectAll();
});
this.selectAllItem = selectAllButton;
}
public getSelectedFiles(): TFile[]
{
let selectedFiles: TFile[] = [];
this.forAllChildren((child) =>
{
if(child.checked && !child.isFolder) selectedFiles.push(child.file);
});
return selectedFiles;
}
public getSelectedFilesSavePaths(): string[]
{
let selectedFiles: string[] = [];
if (this.selectAllItem?.checked)
{
selectedFiles = ["all"];
return selectedFiles;
}
this.forAllChildren((child) =>
{
selectedFiles.push(...child.getSelectedFilesSavePaths());
}, false);
return selectedFiles;
}
public setSelectedFiles(files: string[])
{
if (files.includes("all"))
{
this.selectAllItem?.check(true, false, true);
this.forAllChildren((child) => child.check(true));
return;
}
this.forAllChildren((child) =>
{
if(files.includes(child.href ?? ""))
{
child.check(true);
}
});
this.evaluateFolderChecks();
}
public forAllChildren(func: (child: FilePickerTreeItem) => void, recursive?: boolean): void {
super.forAllChildren(func, recursive);
}
public evaluateFolderChecks()
{
// if all a folder's children are checked, check the folder, otherwise uncheck it
this.forAllChildren((child) =>
{
if(child.isFolder)
{
let uncheckedChildren = child?.itemEl?.querySelectorAll(".mod-tree-file .file-checkbox:not(.checked)");
if (!child.checked && uncheckedChildren?.length == 0)
{
child.check(true, false, true);
}
else if (uncheckedChildren?.length ?? 0 > 0)
{
child.check(false, false, true);
}
}
});
// if all folders are checked, check the select all button, otherwise uncheck it
if (this.children.reduce((acc, child) => acc && child.checked, true))
{
this.selectAllItem?.check(true, false, true);
}
else
{
this.selectAllItem?.check(false, false, true);
}
}
}
export class FilePickerTreeItem extends FileTreeItem
{
public file: TFile;
public checkbox: HTMLInputElement;
public tree: FilePickerTree;
public checked: boolean = false;
protected async createItemContents(container: HTMLElement): Promise<HTMLDivElement>
{
let linkEl = await super.createItemContents(container);
this.checkbox = linkEl.createEl("input");
linkEl.prepend(this.checkbox);
this.checkbox.classList.add("file-checkbox");
this.checkbox.setAttribute("type", "checkbox");
this.checkbox.addEventListener("click", (event) =>
{
event.stopPropagation();
this.check(this.checkbox.checked, false);
this.tree.evaluateFolderChecks();
});
let localThis = this;
linkEl?.addEventListener("click", function(event)
{
if(localThis.isFolder) localThis.toggleCollapse();
else localThis.toggle(true);
});
return linkEl;
}
public check(checked: boolean, evaluate: boolean = false, skipChildren: boolean = false)
{
this.checked = checked;
this.checkbox.checked = checked;
this.checkbox.classList.toggle("checked", checked);
if (!skipChildren) this.checkAllChildren(checked);
if(evaluate) this.tree.evaluateFolderChecks();
}
public toggle(evaluate = false)
{
this.check(!this.checked, evaluate);
}
public checkAllChildren(checked: boolean)
{
this.forAllChildren((child) => child.check(checked));
}
public forAllChildren(func: (child: FilePickerTreeItem) => void, recursive?: boolean): void
{
super.forAllChildren(func, recursive);
}
public getSelectedFilesSavePaths(): string[]
{
let selectedFiles: string[] = [];
if (this.checked)
{
selectedFiles.push(this.href ?? "");
}
else if (this.isFolder)
{
this.forAllChildren((child) =>
{
selectedFiles.push(...child.getSelectedFilesSavePaths());
}, false);
}
return selectedFiles;
}
}

View File

@ -1,167 +0,0 @@
import { TAbstractFile, TFile, TFolder } from "obsidian";
import { Tree, TreeItem } from "./tree";
import { Path } from "scripts/utils/path";
import { Website } from "./website";
import { MarkdownRendererAPI } from "scripts/render-api";
export class FileTree extends Tree
{
public children: FileTreeItem[] = [];
public showFileExtentionTags: boolean = true;
/** File extentions matching this will not show extention tags */
public hideFileExtentionTags: string[] = [];
public files: TFile[];
public keepOriginalExtensions: boolean;
public sort: boolean;
public constructor(files: TFile[], keepOriginalExtensions: boolean = false, sort = true)
{
super();
this.files = files;
this.keepOriginalExtensions = keepOriginalExtensions;
this.sort = sort;
this.renderMarkdownTitles = true;
}
protected async populateTree()
{
for (let file of this.files)
{
let pathSections: TAbstractFile[] = [];
let parentFile: TAbstractFile = file;
while (parentFile != undefined)
{
pathSections.push(parentFile);
// @ts-ignore
parentFile = parentFile.parent;
}
pathSections.reverse();
let parent: FileTreeItem | FileTree = this;
for (let i = 1; i < pathSections.length; i++)
{
let section = pathSections[i];
let isFolder = section instanceof TFolder;
// make sure this section hasn't already been added
let child = parent.children.find(sibling => sibling.title == section.name && sibling.isFolder == isFolder && sibling.depth == i) as FileTreeItem | undefined;
if (child == undefined)
{
child = new FileTreeItem(this, parent, i);
child.title = section.name;
child.isFolder = isFolder;
if(child.isFolder)
{
child.itemClass = "mod-tree-folder nav-folder";
let titleInfo = await Website.getTitleAndIcon(section);
child.icon = titleInfo.icon;
}
else child.itemClass = "mod-tree-file nav-file"
parent.children.push(child);
}
parent = child;
}
if (parent instanceof FileTreeItem)
{
let titleInfo = await Website.getTitleAndIcon(file);
let path = new Path(file.path).makeUnixStyle();
if (file instanceof TFolder) path.makeForceFolder();
else
{
if (path.asString.endsWith(".excalidraw.md")) path.setExtension("drawing");
parent.originalExtension = path.extensionName;
if(!this.keepOriginalExtensions && MarkdownRendererAPI.isConvertable(path.extensionName)) path.setExtension("html");
}
parent.href = path.asString;
parent.title = path.basename == "." ? "" : titleInfo.title;
parent.icon = titleInfo.icon || "";
}
}
if (this.sort)
{
this.sortAlphabetically();
this.sortByIsFolder();
}
}
public override async generateTree(container: HTMLElement): Promise<void>
{
await this.populateTree();
await super.generateTree(container);
}
public sortByIsFolder(reverse: boolean = false)
{
this.children.sort((a, b) => reverse ? (a.isFolder && !b.isFolder ? -1 : 1) : (a.isFolder && !b.isFolder ? -1 : 1));
for (let child of this.children)
{
child.sortByIsFolder(reverse);
}
}
public forAllChildren(func: (child: FileTreeItem) => void, recursive: boolean = true)
{
for (let child of this.children)
{
func(child);
if (recursive) child.forAllChildren(func);
}
}
}
export class FileTreeItem extends TreeItem
{
public tree: FileTree;
public children: FileTreeItem[] = [];
public parent: FileTreeItem | FileTree;
public isFolder = false;
public originalExtension: string = "";
public forAllChildren(func: (child: FileTreeItem) => void, recursive: boolean = true)
{
super.forAllChildren(func, recursive);
}
public sortByIsFolder(reverse: boolean = false)
{
this.children.sort((a, b) => reverse ? (a.isFolder && !b.isFolder ? -1 : 1) : (a.isFolder && !b.isFolder ? -1 : 1));
for (let child of this.children)
{
child.sortByIsFolder(reverse);
}
}
protected override async createItemContents(container: HTMLElement): Promise<HTMLDivElement>
{
let containerEl = await super.createItemContents(container);
if (this.isFolder) containerEl.addClass("nav-folder-title");
else containerEl.addClass("nav-file-title");
if (!this.isFolder && this.tree.showFileExtentionTags && !this.tree.hideFileExtentionTags.contains(this.originalExtension))
{
let tag = containerEl.createDiv({ cls: "nav-file-tag" });
tag.textContent = this.originalExtension.toUpperCase();
}
return containerEl;
}
protected override async createItemTitle(container: HTMLElement): Promise<HTMLSpanElement>
{
let titleEl = await super.createItemTitle(container);
if (this.isFolder) titleEl.addClass("nav-folder-title-content", "tree-item-inner");
else titleEl.addClass("nav-file-title-content", "tree-item-inner");
return titleEl;
}
}

View File

@ -1,129 +0,0 @@
import { TFile } from "obsidian";
import { Path } from "scripts/utils/path";
import { Settings } from "scripts/settings/settings";
import { Website } from "./website";
import { GraphViewOptions, MarkdownWebpageRendererAPIOptions } from "scripts/api-options";
export class GraphView
{
public nodeCount: number;
public linkCount: number;
public radii: number[];
public labels: string[];
public paths: string[];
public linkSources: number[];
public linkTargets: number[];
public graphOptions: GraphViewOptions = new GraphViewOptions();
private isInitialized: boolean = false;
static InOutQuadBlend(start: number, end: number, t: number): number
{
t /= 2;
let t2 = 2.0 * t * (1.0 - t) + 0.5;
t2 -= 0.5;
t2 *= 2.0;
return start + (end - start) * t2;
}
public async init(files: TFile[], options: MarkdownWebpageRendererAPIOptions)
{
if (this.isInitialized) return;
Object.assign(this.graphOptions, options.graphViewOptions);
this.paths = files.map(f => f.path);
this.nodeCount = this.paths.length;
this.linkSources = [];
this.linkTargets = [];
this.labels = [];
this.radii = [];
let linkCounts: number[] = [];
for (let i = 0; i < this.nodeCount; i++)
{
linkCounts.push(0);
}
let resolvedLinks = Object.entries(app.metadataCache.resolvedLinks);
let values = Array.from(resolvedLinks.values());
let sources = values.map(v => v[0]);
let targets = values.map(v => v[1]);
for (let source of this.paths)
{
let sourceIndex = sources.indexOf(source);
let file = files.find(f => f.path == source);
if (file)
{
let titleInfo = await Website.getTitleAndIcon(file, true);
this.labels.push(titleInfo.title);
}
if (sourceIndex != -1)
{
let target = targets[sourceIndex];
for (let link of Object.entries(target))
{
if (link[0] == source) continue;
if (this.paths.includes(link[0]))
{
let path1 = source;
let path2 = link[0];
let index1 = this.paths.indexOf(path1);
let index2 = this.paths.indexOf(path2);
if (index1 == -1 || index2 == -1) continue;
this.linkSources.push(index1);
this.linkTargets.push(index2);
linkCounts[index1] = (linkCounts[index1] ?? 0) + 1;
linkCounts[index2] = (linkCounts[index2] ?? 0) + 1;
}
}
}
}
let maxLinks = Math.max(...linkCounts);
this.radii = linkCounts.map(l => GraphView.InOutQuadBlend(this.graphOptions.minNodeRadius, this.graphOptions.maxNodeRadius, Math.min(l / (maxLinks * 0.8), 1.0)));
this.paths = this.paths.map(p => new Path(p).setExtension(".html").makeUnixStyle().makeWebStyle(options.webStylePaths).asString);
this.linkCount = this.linkSources.length;
this.isInitialized = true;
}
public static generateGraphEl(container: HTMLElement): HTMLElement
{
let graphWrapper = container.createDiv();
graphWrapper.classList.add("graph-view-wrapper");
let graphHeader = graphWrapper.createDiv();
graphHeader.addClass("sidebar-section-header");
graphHeader.innerText = "Interactive Graph";
let graphEl = graphWrapper.createDiv();
graphEl.className = "graph-view-placeholder";
graphEl.innerHTML =
`
<div class="graph-view-container">
<div class="graph-icon graph-expand" role="button" aria-label="Expand" data-tooltip-position="top"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon"><line x1="7" y1="17" x2="17" y2="7"></line><polyline points="7 7 17 7 17 17"></polyline></svg></div>
<canvas id="graph-canvas" class="hide" width="512px" height="512px"></canvas>
</div>
`
return graphWrapper;
}
public getExportData(): string
{
if (!this.isInitialized) throw new Error("Graph not initialized");
return `let graphData=\n${JSON.stringify(this)};`;
}
}

View File

@ -1,99 +0,0 @@
import { TFile } from "obsidian";
import { Tree, TreeItem } from "./tree";
import { Webpage } from "./webpage";
export class OutlineTree extends Tree
{
public file: TFile;
public webpage: Webpage;
public children: OutlineTreeItem[];
public minDepth: number = 1;
public depth: number = 0;
private createTreeItem(heading: {heading: string, level: number, headingEl: HTMLElement}, parent: OutlineTreeItem | OutlineTree): OutlineTreeItem
{
let item = new OutlineTreeItem(this, parent, heading);
item.title = heading.heading;
return item;
}
public constructor(webpage: Webpage, minDepth = 1)
{
super();
this.webpage = webpage;
this.file = webpage.source;
this.minDepth = minDepth;
let headings = webpage.headings;
this.depth = Math.min(...headings.map(h => h.level)) - 1;
let parent: OutlineTreeItem | OutlineTree = this;
for (let heading of headings)
{
if (heading.level < minDepth) continue;
if (heading.level > parent.depth)
{
let child = this.createTreeItem(heading, parent);
parent.children.push(child);
if(heading.level == parent.depth + 1) parent = child;
}
else if (heading.level == parent.depth)
{
if(parent instanceof OutlineTreeItem)
{
let child = this.createTreeItem(heading, parent.parent);
parent.parent.children.push(child);
parent = child;
}
}
else if (heading.level < parent.depth)
{
if (parent instanceof OutlineTreeItem)
{
let levelChange = parent.depth - heading.level;
let backParent: OutlineTreeItem | OutlineTree = (parent.parent as OutlineTreeItem | OutlineTree) ?? parent;
for (let i = 0; i < levelChange; i++)
{
if (backParent instanceof OutlineTreeItem) backParent = (backParent.parent as OutlineTreeItem | OutlineTree) ?? backParent;
}
let child = this.createTreeItem(heading, backParent);
backParent.children.push(child);
parent = child;
}
}
}
}
}
export class OutlineTreeItem extends TreeItem
{
public children: OutlineTreeItem[] = [];
public parent: OutlineTreeItem | OutlineTree;
public heading: string;
public constructor(tree: OutlineTree, parent: OutlineTreeItem | OutlineTree, heading: {heading: string, level: number, headingEl: HTMLElement})
{
super(tree, parent, heading.level);
this.heading = heading.heading;
this.href = tree.webpage.relativePath + "#" + heading.headingEl.id;
}
public forAllChildren(func: (child: OutlineTreeItem) => void, recursive: boolean = true)
{
super.forAllChildren(func, recursive);
}
protected override async createItemContents(container: HTMLElement): Promise<HTMLDivElement>
{
let linkEl = await super.createItemContents(container);
linkEl?.setAttribute("heading-name", this.heading);
linkEl.classList.add("heading-link");
return linkEl;
}
}

View File

@ -1,345 +0,0 @@
import { MarkdownRendererAPI } from "scripts/render-api";
import { Path } from "scripts/utils/path";
export class Tree
{
public children: TreeItem[] = [];
public minCollapsableDepth: number = 1;
public title: string = "Tree";
public class: string = "mod-tree-none";
public showNestingIndicator = true;
public minDepth: number = 1;
public generateWithItemsClosed: boolean = false;
public makeLinksWebStyle: boolean = false;
public renderMarkdownTitles: boolean = true;
public container: HTMLElement | undefined = undefined;
protected async buildTreeRecursive(tree: TreeItem, container: HTMLElement, minDepth:number = 1, closeAllItems: boolean = false): Promise<void>
{
tree.minCollapsableDepth = this.minCollapsableDepth;
let treeItem = await tree.generateItemHTML(container, closeAllItems);
if(!tree.childContainer) return;
for (let item of tree.children)
{
if(item.depth < minDepth) continue;
await this.buildTreeRecursive(item, tree.childContainer, minDepth, closeAllItems);
}
}
//**Generate the raw tree with no extra containers or buttons*/
public async generateTree(container: HTMLElement)
{
for (let item of this.children)
{
await this.buildTreeRecursive(item, container, this.minDepth, this.generateWithItemsClosed);
}
this.forAllChildren((child) =>
{
if (child.isCollapsed) child.setCollapse(true, false);
});
}
//**Generate a tree with a title and full tree collapse button*/
public async generateTreeWithContainer(container: HTMLElement)
{
/*
- div.tree-container.mod-root.nav-folder
- div.tree-header
- span.sidebar-section-header
- button.collapse-tree-button
- svg
- div.tree-scroll-area.tree-item-children
- div.tree-item // invisible first item
- div.tree-item
- div.tree-item-contents
- div.tree-item-icon
- svg
- a.internal-link
- span.tree-item-title
- div.tree-item-children
*/
this.container = container;
let treeContainerEl = container.createDiv();
let treeHeaderEl = container.createDiv();
let sectionHeaderEl = container.createEl('span');
let collapseAllEl = container.createEl('button');
let treeScrollAreaEl = container.createDiv();
treeContainerEl.classList.add('tree-container', "mod-root", "nav-folder", "tree-item", this.class);
treeHeaderEl.classList.add("tree-header");
sectionHeaderEl.classList.add("sidebar-section-header");
collapseAllEl.classList.add("clickable-icon", "collapse-tree-button");
collapseAllEl.setAttribute("aria-label", "Collapse All");
collapseAllEl.innerHTML = "<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'></svg>";
treeScrollAreaEl.classList.add("tree-scroll-area", "tree-item-children", "nav-folder-children");
let invisFirst = treeScrollAreaEl.createDiv("tree-item mod-tree-folder nav-folder mod-collapsible is-collapsed"); // invisible first item
invisFirst.style.display = "none";
if (this.generateWithItemsClosed) collapseAllEl.classList.add("is-collapsed");
if (this.showNestingIndicator) treeContainerEl.classList.add("mod-nav-indicator");
treeContainerEl.setAttribute("data-depth", "0");
sectionHeaderEl.innerText = this.title;
treeContainerEl.appendChild(treeHeaderEl);
treeContainerEl.appendChild(treeScrollAreaEl);
treeHeaderEl.appendChild(sectionHeaderEl);
treeHeaderEl.appendChild(collapseAllEl);
await this.generateTree(treeScrollAreaEl);
}
public sortAlphabetically(reverse: boolean = false)
{
this.children.sort((a, b) => reverse ? b.title.localeCompare(a.title, undefined, { numeric: true }) : a.title.localeCompare(b.title, undefined, { numeric: true }));
for (let child of this.children)
{
child.sortAlphabetically();
}
}
public forAllChildren(func: (child: TreeItem) => void, recursive: boolean = true)
{
for (let child of this.children)
{
func(child);
if (recursive) child.forAllChildren(func);
}
}
}
export class TreeItem
{
public tree: Tree;
public children: TreeItem[] = [];
public parent: TreeItem | Tree;
public depth: number = 0;
public itemClass: string = "";
public title: string = "";
public icon: string = "";
public href: string | undefined = undefined;
public minCollapsableDepth: number = 1;
public isCollapsed: boolean = false;
public childContainer: HTMLDivElement | undefined = undefined;
public itemEl: HTMLDivElement | undefined = undefined;
public constructor(tree: Tree, parent: TreeItem | Tree, depth: number)
{
this.tree = tree;
this.parent = parent;
this.depth = depth;
}
public async generateItemHTML(container: HTMLElement, startClosed: boolean = true): Promise<HTMLDivElement>
{
/*
- div.tree-item-wrapper
- div.a.tree-link
- .tree-item-contents
- div.tree-item-icon
- svg
- span.tree-item-title
- div.tree-item-children
*/
if(startClosed) this.isCollapsed = true;
this.itemEl = this.createItemWrapper(container);
await this.createItemLink(this.itemEl);
this.createItemChildren(this.itemEl);
return this.itemEl;
}
public forAllChildren(func: (child: TreeItem) => void, recursive: boolean = true)
{
for (let child of this.children)
{
func(child);
if (recursive) child.forAllChildren(func);
}
}
public async setCollapse(collapsed: boolean, animate = true)
{
if (!this.isCollapsible()) return;
if (!this.itemEl || !this.itemEl.classList.contains("mod-collapsible")) return;
let children = this.itemEl.querySelector(".tree-item-children") as HTMLElement;
if (children == null) return;
if (collapsed)
{
this.itemEl.classList.add("is-collapsed");
if(animate) this.slideUp(children, 100);
else children.style.display = "none";
}
else
{
this.itemEl.classList.remove("is-collapsed");
if(animate) this.slideDown(children, 100);
else children.style.removeProperty("display");
}
this.isCollapsed = collapsed;
}
public toggleCollapse()
{
if (!this.itemEl) return;
this.setCollapse(!this.itemEl.classList.contains("is-collapsed"));
}
public sortAlphabetically(reverse: boolean = false)
{
this.children.sort((a, b) => reverse ? b.title.localeCompare(a.title, undefined, { numeric: true }) : a.title.localeCompare(b.title, undefined, { numeric: true }));
for (let child of this.children)
{
child.sortAlphabetically();
}
}
protected isCollapsible(): boolean
{
return this.children.length != 0 && this.depth >= this.minCollapsableDepth;
}
protected createItemWrapper(container: HTMLElement): HTMLDivElement
{
let itemEl = container.createDiv();
itemEl.classList.add("tree-item");
if (this.itemClass.trim() != "") itemEl.classList.add(...this.itemClass.split(" "));
itemEl.setAttribute("data-depth", this.depth.toString());
if (this.isCollapsible()) itemEl.classList.add("mod-collapsible");
return itemEl;
}
protected async createItemContents(container: HTMLElement): Promise<HTMLDivElement>
{
let itemContentsEl = container.createDiv("tree-item-contents");
if (this.isCollapsible())
{
this.createItemCollapseIcon(itemContentsEl);
if (this.isCollapsed)
{
this.itemEl?.classList.add("is-collapsed");
}
}
this.createItemIcon(itemContentsEl);
await this.createItemTitle(itemContentsEl);
return itemContentsEl;
}
protected async createItemLink(container: HTMLElement): Promise<{ linkEl: HTMLElement, contentEl: HTMLSpanElement }>
{
if (this.tree.makeLinksWebStyle && this.href) this.href = Path.toWebStyle(this.href);
let itemLinkEl = container.createEl(this.href ? "a" : "div", { cls: "tree-link" });
if (this.href) itemLinkEl.setAttribute("href", this.href);
let itemContentEl = await this.createItemContents(itemLinkEl);
return { linkEl: itemLinkEl, contentEl: itemContentEl };
}
protected createItemCollapseIcon(container: HTMLElement): HTMLElement | undefined
{
const arrowIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon right-triangle"><path d="M3 8L12 17L21 8"></path></svg>`;
let itemIconEl = container.createDiv("collapse-icon");
itemIconEl.innerHTML = arrowIcon;
return itemIconEl;
}
protected async createItemTitle(container: HTMLElement): Promise<HTMLSpanElement>
{
let titleEl = container.createEl("span", { cls: "tree-item-title" });
if (this.tree.renderMarkdownTitles) MarkdownRendererAPI.renderMarkdownSimpleEl(this.title, titleEl);
else titleEl.innerText = this.title;
return titleEl;
}
protected createItemIcon(container: HTMLElement): HTMLDivElement | undefined
{
if (this.icon.trim() == "") return undefined;
let itemIconEl = container.createDiv("tree-item-icon");
if (this.tree.renderMarkdownTitles) MarkdownRendererAPI.renderMarkdownSimpleEl(this.icon, itemIconEl);
else itemIconEl.innerText = this.icon;
return itemIconEl;
}
protected createItemChildren(container: HTMLElement): HTMLDivElement
{
this.childContainer = container.createDiv("tree-item-children nav-folder-children");
return this.childContainer;
}
protected slideUp(target: HTMLElement, duration=500)
{
target.style.transitionProperty = 'height, margin, padding';
target.style.transitionDuration = duration + 'ms';
target.style.boxSizing = 'border-box';
target.style.height = target.offsetHeight + 'px';
target.offsetHeight;
target.style.overflow = 'hidden';
target.style.height = "0";
target.style.paddingTop = "0";
target.style.paddingBottom = "0";
target.style.marginTop = "0";
target.style.marginBottom = "0";
window.setTimeout(async () => {
target.style.display = 'none';
target.style.removeProperty('height');
target.style.removeProperty('padding-top');
target.style.removeProperty('padding-bottom');
target.style.removeProperty('margin-top');
target.style.removeProperty('margin-bottom');
target.style.removeProperty('overflow');
target.style.removeProperty('transition-duration');
target.style.removeProperty('transition-property');
}, duration);
}
protected slideDown(target: HTMLElement, duration=500)
{
target.style.removeProperty('display');
let display = window.getComputedStyle(target).display;
if (display === 'none') display = 'block';
target.style.display = display;
let height = target.offsetHeight;
target.style.overflow = 'hidden';
target.style.height = "0";
target.style.paddingTop = "0";
target.style.paddingBottom = "0";
target.style.marginTop = "0";
target.style.marginBottom = "0";
target.offsetHeight;
target.style.boxSizing = 'border-box';
target.style.transitionProperty = "height, margin, padding";
target.style.transitionDuration = duration + 'ms';
target.style.height = height + 'px';
target.style.removeProperty('padding-top');
target.style.removeProperty('padding-bottom');
target.style.removeProperty('margin-top');
target.style.removeProperty('margin-bottom');
window.setTimeout(async () => {
target.style.removeProperty('height');
target.style.removeProperty('overflow');
target.style.removeProperty('transition-duration');
target.style.removeProperty('transition-property');
}, duration);
}
}

View File

@ -1,797 +0,0 @@
import { FrontMatterCache, TFile } from "obsidian";
import { Path } from "scripts/utils/path";
import { Downloadable } from "scripts/utils/downloadable";
import { OutlineTree } from "./outline-tree";
import { GraphView } from "./graph-view";
import { Website } from "./website";
import { AssetHandler } from "scripts/html-generation/asset-handler";
import { HTMLGeneration } from "scripts/html-generation/html-generation-helpers";
import { Utils } from "scripts/utils/utils";
import { ExportLog } from "scripts/html-generation/render-log";
import { MarkdownRendererAPI } from "scripts/render-api";
import { MarkdownWebpageRendererAPIOptions } from "scripts/api-options";
const { minify } = require('html-minifier-terser');
export class Webpage extends Downloadable
{
/**
* The original file this webpage was exported from
*/
public source: TFile;
public website: Website | undefined;
/**
* The document containing this webpage's HTML
*/
public document?: Document;
/**
* The absolute path to the ROOT FOLDER of the export
*/
public destinationFolder: Path;
/**
* The external files that need to be downloaded for this file to work NOT including the file itself.
*/
public dependencies: Downloadable[] = [];
public viewType: string = "markdown";
public isConvertable: boolean = false;
public exportOptions: MarkdownWebpageRendererAPIOptions;
public title: string = "";
public icon: string = "";
public titleInfo: {title: string, icon: string, isDefaultTitle: boolean, isDefaultIcon: boolean} = {title: "", icon: "", isDefaultTitle: true, isDefaultIcon: true};
/**
* @param file The original markdown file to export
* @param destination The absolute path to the FOLDER we are exporting to
* @param name The name of the file being exported without the extension
* @param website The website this file is part of
* @param options The options for exporting this file
*/
constructor(file: TFile, destination?: Path, name?: string, website?: Website, options?: MarkdownWebpageRendererAPIOptions)
{
if(destination && (!destination.isAbsolute || !destination.isDirectory)) throw new Error("destination must be an absolute directory path: " + destination?.asString);
super(file.basename, "", Path.emptyPath);
options = Object.assign(new MarkdownWebpageRendererAPIOptions(), options);
let isConvertable = MarkdownRendererAPI.isConvertable(file.extension);
this.filename = name ?? file.basename;
this.filename += isConvertable ? ".html" : "." + file.extension;
this.isConvertable = isConvertable;
this.exportOptions = options;
this.source = file;
this.website = website ?? undefined;
this.destinationFolder = destination ?? Path.vaultPath.joinString("Export");
if (this.isConvertable) this.document = document.implementation.createHTMLDocument(this.source.basename);
let sourcePath = new Path(file.path);
this.relativeDirectory = sourcePath.directory;
if (this.exportOptions.flattenExportPaths)
this.relativeDirectory = Path.emptyPath;
if (this.exportOptions.webStylePaths)
{
this.filename = Path.toWebStyle(this.filename);
this.relativeDirectory.makeWebStyle();
}
}
/**
* The HTML string for the file
*/
get html(): string
{
let htmlString = "<!DOCTYPE html> " + this.document?.documentElement.outerHTML;
return htmlString;
}
/**
* The element that contains the content of the document, aka the markdown-preview-view or view-content
*/
get viewElement(): HTMLElement
{
if (!this.document) throw new Error("Document is not defined");
let viewContent = this.document.querySelector(".view-content") as HTMLElement;
let markdownPreview = this.document.querySelector(".markdown-preview-view") as HTMLElement;
if (!viewContent && !markdownPreview)
{
throw new Error("No content element found");
}
if (this.viewType != "markdown")
return viewContent ?? markdownPreview;
return markdownPreview ?? viewContent;
}
/**
* The element that determines the size of the document, aka the markdown-preview-sizer
*/
get sizerElement(): HTMLDivElement
{
if (this.viewType != "markdown") return this.document?.querySelector(".view-content")?.firstChild as HTMLDivElement;
return this.document?.querySelector(".markdown-preview-sizer") as HTMLDivElement;
}
/**
* The absolute path that the file will be saved to
*/
get exportPath(): Path
{
return this.destinationFolder?.join(this.relativePath) ?? Path.vaultPath.join(this.relativePath);
}
/**
* The relative path from exportPath to rootFolder
*/
get pathToRoot(): Path
{
return Path.getRelativePath(this.relativePath, new Path(this.relativePath.workingDirectory), true).makeUnixStyle();
}
get tags(): string[]
{
let tagCaches = app.metadataCache.getFileCache(this.source)?.tags?.values();
if (tagCaches)
{
let tags = Array.from(tagCaches).map((tag) => tag.tag);
return tags;
}
return [];
}
get headings(): {heading: string, level: number, headingEl: HTMLElement}[]
{
let headers: {heading: string, level: number, headingEl: HTMLElement}[] = [];
if (this.document)
{
this.document.querySelectorAll(".heading").forEach((headerEl: HTMLElement) =>
{
let level = parseInt(headerEl.tagName[1]);
if (headerEl.closest("[class^='block-language-']") || headerEl.closest(".markdown-embed.inline-embed")) level += 6;
let heading = headerEl.getAttribute("data-heading") ?? headerEl.innerText ?? "";
headers.push({heading, level, headingEl: headerEl});
});
}
return headers;
}
get aliases(): string[]
{
let aliases = this.frontmatter?.aliases ?? [];
return aliases;
}
get description(): string
{
return this.frontmatter["description"] || this.frontmatter["summary"] || "";
}
get author(): string
{
return this.frontmatter["author"] || this.exportOptions.authorName || "";
}
get fullURL(): string
{
let url = Path.joinStrings(this.exportOptions.siteURL ?? "", this.relativePath.asString).makeUnixStyle().asString;
return url;
}
get metadataImageURL(): string | undefined
{
let mediaPathStr = this.viewElement.querySelector("img")?.getAttribute("src") ?? "";
let hasMedia = mediaPathStr.length > 0;
if (!hasMedia) return undefined;
if (!mediaPathStr.startsWith("http") && !mediaPathStr.startsWith("data:"))
{
let mediaPath = Path.joinStrings(this.exportOptions.siteURL ?? "", mediaPathStr);
mediaPathStr = mediaPath.asString;
}
return mediaPathStr;
}
get frontmatter(): FrontMatterCache
{
let frontmatter = app.metadataCache.getFileCache(this.source)?.frontmatter ?? {};
return frontmatter;
}
public getCompatibilityContent(): string
{
let oldContent = this.sizerElement.outerHTML;
let compatContent = this.sizerElement;
compatContent.querySelectorAll("script, style, .collapse-indicator, .callout-icon, .icon, a.tag").forEach((script) => script.remove());
function moveChildrenOut(element: HTMLElement)
{
let children = Array.from(element.children);
element.parentElement?.append(...children);
element.remove();
}
let headingTreeElements = Array.from(compatContent.querySelectorAll(".heading-wrapper"));
headingTreeElements.forEach(moveChildrenOut);
headingTreeElements = Array.from(compatContent.querySelectorAll(".heading-children"));
headingTreeElements.forEach(moveChildrenOut);
let lowDivs = Array.from(compatContent.children).filter((el) => el.tagName == "DIV" && el.childElementCount == 1);
lowDivs.forEach(moveChildrenOut);
let all = Array.from(compatContent.querySelectorAll("*"));
all.forEach((el: HTMLElement) =>
{
// give default var values
let fillDefault = el.tagName == "text" ? "#181818" : "white";
el.style.fill = el.style.fill.replace(/var\(([\w -]+)\)/g, `var($1, ${fillDefault})`);
el.style.stroke = el.style.stroke.replace(/var\(([\w -]+)\)/g, "var($1, #181818)");
el.style.backgroundColor = el.style.backgroundColor.replace(/var\(([\w -]+)\)/g, "var($1, white)");
el.style.color = el.style.color.replace(/var\(([\w -]+)\)/g, "var($1, #181818)");
el.removeAttribute("id");
el.removeAttribute("class");
el.removeAttribute("font-family");
});
let result = compatContent.innerHTML;
compatContent.innerHTML = oldContent;
result = result.replaceAll("<", " <");
return result;
}
public async create(): Promise<Webpage | undefined>
{
this.titleInfo = await Website.getTitleAndIcon(this.source);
this.title = this.titleInfo.title;
this.icon = this.titleInfo.icon;
if (!this.isConvertable)
{
this.content = await new Path(this.source.path).readFileBuffer() ?? "";
this.modifiedTime = this.source.stat.mtime;
return this;
}
if (!this.document) return this;
let webpageWithContent = await this.populateDocument();
if(!webpageWithContent)
{
if (!MarkdownRendererAPI.checkCancelled()) ExportLog.error(this.source, "Failed to create webpage");
return;
}
if (this.exportOptions.addHeadTag)
await this.addHead();
if (this.exportOptions.addTitle)
await this.addTitle();
if (this.exportOptions.addSidebars)
{
let innerContent = this.document.body.innerHTML;
this.document.body.innerHTML = "";
let layout = this.generateWebpageLayout(innerContent);
this.document.body.appendChild(layout.container);
let rightSidebar = layout.right;
let leftSidebar = layout.left;
// inject graph view
if (this.exportOptions.addGraphView)
{
GraphView.generateGraphEl(rightSidebar);
}
// inject outline
if (this.exportOptions.addOutline)
{
let headerTree = new OutlineTree(this, 1);
headerTree.class = "outline-tree";
headerTree.title = "Table Of Contents";
headerTree.showNestingIndicator = false;
headerTree.generateWithItemsClosed = this.exportOptions.startOutlineCollapsed === true;
headerTree.minCollapsableDepth = this.exportOptions.minOutlineCollapsibleLevel ?? 2;
await headerTree.generateTreeWithContainer(rightSidebar);
}
// inject darkmode toggle
if (this.exportOptions.addThemeToggle)
{
HTMLGeneration.createThemeToggle(layout.leftBar);
}
// inject search bar
// TODO: don't hardcode searchbar html
if (this.exportOptions.addSearch)
{
let searchbarHTML = `<div class="search-input-container"><input enterkeyhint="search" type="search" spellcheck="false" placeholder="Search..."><div class="search-input-clear-button" aria-label="Clear search"></div></div>`;
leftSidebar.createDiv().outerHTML = searchbarHTML;
}
// inject file tree
if (this.website && this.exportOptions.addFileNavigation)
{
leftSidebar.createDiv().outerHTML = this.website.fileTreeAsset.getHTML(this.exportOptions);
// if the file will be opened locally, un-collapse the tree containing this file
if (this.exportOptions.openNavFileLocation)
{
let sidebar = leftSidebar.querySelector(".file-tree");
let unixPath = this.relativePath.copy.makeUnixStyle().asString;
let fileElement: HTMLElement = sidebar?.querySelector(`[href="${unixPath}"]`) as HTMLElement;
fileElement = fileElement?.closest(".tree-item") as HTMLElement;
while (fileElement)
{
fileElement?.classList.remove("is-collapsed");
let children = fileElement?.querySelector(".tree-item-children") as HTMLElement;
if(children) children.style.display = "block";
fileElement = fileElement?.parentElement?.closest(".tree-item") as HTMLElement;
}
}
}
}
if (this.exportOptions.includeJS)
{
let bodyScript = this.document.body.createEl("script");
bodyScript.setAttribute("defer", "");
bodyScript.innerText = AssetHandler.themeLoadJS.content.toString();
this.document.body.prepend(bodyScript);
}
this.content = this.html;
return this;
}
private async populateDocument(): Promise<Webpage | undefined>
{
if (!this.isConvertable || !this.document) return this;
let body = this.document.body;
if (this.exportOptions.addBodyClasses)
body.setAttribute("class", Website.validBodyClasses || await HTMLGeneration.getValidBodyClasses(false));
let options = {...this.exportOptions, container: body};
let renderInfo = await MarkdownRendererAPI.renderFile(this.source, options);
let contentEl = renderInfo?.contentEl;
this.viewType = renderInfo?.viewType ?? "markdown";
if (!contentEl) return undefined;
if (MarkdownRendererAPI.checkCancelled()) return undefined;
if (this.viewType == "markdown")
{
contentEl.classList.toggle("allow-fold-headings", this.exportOptions.allowFoldingHeadings);
contentEl.classList.toggle("allow-fold-lists", this.exportOptions.allowFoldingLists);
contentEl.classList.add("is-readable-line-width");
}
if(this.sizerElement) this.sizerElement.style.paddingBottom = "";
// modify links to work outside of obsidian (including relative links)
if (this.exportOptions.fixLinks)
this.convertLinks();
// add math styles to the document. They are here and not in <head> because they are unique to each document
if (this.exportOptions.addMathjaxStyles)
{
let mathStyleEl = document.createElement("style");
mathStyleEl.id = "MJX-CHTML-styles";
await AssetHandler.mathjaxStyles.load(this.exportOptions);
mathStyleEl.innerHTML = AssetHandler.mathjaxStyles.content;
this.viewElement.prepend(mathStyleEl);
}
// inline / outline images
let outlinedImages : Downloadable[] = [];
if (this.exportOptions.inlineMedia)
await this.inlineMedia();
else
outlinedImages = await this.exportMedia();
this.dependencies.push(...outlinedImages);
if(this.exportOptions.webStylePaths)
{
this.dependencies.forEach((file) =>
{
file.filename = Path.toWebStyle(file.filename);
file.relativeDirectory = file.relativeDirectory?.makeWebStyle();
});
}
return this;
}
private generateWebpageLayout(middleContent: HTMLElement | Node | string): {container: HTMLElement, left: HTMLElement, leftBar: HTMLElement, right: HTMLElement, rightBar: HTMLElement, center: HTMLElement}
{
if (!this.document) throw new Error("Document is not defined");
/*
- div.webpage-container
- div.sidebar.sidebar-left
- div.sidebar-content
- div.sidebar-topbar
- div.clickable-icon.sidebar-collapse-icon
- svg
- div.document-container
- div.sidebar.sidebar-right
- div.sidebar-content
- div.sidebar-topbar
- div.clickable-icon.sidebar-collapse-icon
- svg
*/
let collapseSidebarIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" class="svg-icon"><path d="M21 3H3C1.89543 3 1 3.89543 1 5V19C1 20.1046 1.89543 21 3 21H21C22.1046 21 23 20.1046 23 19V5C23 3.89543 22.1046 3 21 3Z"></path><path d="M10 4V20"></path><path d="M4 7H7"></path><path d="M4 10H7"></path><path d="M4 13H7"></path></svg>`
let pageContainer = this.document.createElement("div");
let leftSidebar = this.document.createElement("div");
let leftSidebarHandle = this.document.createElement("div");
let leftContent = this.document.createElement("div");
let leftTopbar = this.document.createElement("div");
let leftTopbarContent = this.document.createElement("div");
let leftCollapseIcon = this.document.createElement("div");
let documentContainer = this.document.createElement("div");
let rightSidebar = this.document.createElement("div");
let rightSidebarHandle = this.document.createElement("div");
let rightContent = this.document.createElement("div");
let rightTopbar = this.document.createElement("div");
let rightTopbarContent = this.document.createElement("div");
let rightCollapseIcon = this.document.createElement("div");
pageContainer.setAttribute("class", "webpage-container workspace");
leftSidebar.setAttribute("class", "sidebar-left sidebar");
leftSidebarHandle.setAttribute("class", "sidebar-handle");
leftContent.setAttribute("class", "sidebar-content");
leftTopbar.setAttribute("class", "sidebar-topbar");
leftTopbarContent.setAttribute("class", "topbar-content");
leftCollapseIcon.setAttribute("class", "clickable-icon sidebar-collapse-icon");
documentContainer.setAttribute("class", "document-container markdown-reading-view");
if (this.exportOptions.includeJS) documentContainer.classList.add("hide"); // if js included, hide the content until the js is loaded
rightSidebar.setAttribute("class", "sidebar-right sidebar");
rightSidebarHandle.setAttribute("class", "sidebar-handle");
rightContent.setAttribute("class", "sidebar-content");
rightTopbar.setAttribute("class", "sidebar-topbar");
rightTopbarContent.setAttribute("class", "topbar-content");
rightCollapseIcon.setAttribute("class", "clickable-icon sidebar-collapse-icon");
pageContainer.appendChild(leftSidebar);
pageContainer.appendChild(documentContainer);
pageContainer.appendChild(rightSidebar);
if (this.exportOptions.allowResizeSidebars && this.exportOptions.includeJS) leftSidebar.appendChild(leftSidebarHandle);
leftSidebar.appendChild(leftTopbar);
leftSidebar.appendChild(leftContent);
leftTopbar.appendChild(leftTopbarContent);
leftTopbar.appendChild(leftCollapseIcon);
leftCollapseIcon.innerHTML = collapseSidebarIcon;
documentContainer.innerHTML += middleContent instanceof HTMLElement ? middleContent.outerHTML : middleContent.toString();
if (this.exportOptions.allowResizeSidebars && this.exportOptions.includeJS) rightSidebar.appendChild(rightSidebarHandle);
rightSidebar.appendChild(rightTopbar);
rightSidebar.appendChild(rightContent);
rightTopbar.appendChild(rightTopbarContent);
rightTopbar.appendChild(rightCollapseIcon);
rightCollapseIcon.innerHTML = collapseSidebarIcon;
let leftSidebarScript = leftSidebar.createEl("script");
let rightSidebarScript = rightSidebar.createEl("script");
leftSidebarScript.setAttribute("defer", "");
rightSidebarScript.setAttribute("defer", "");
leftSidebarScript.innerHTML = `let ls = document.querySelector(".sidebar-left"); ls.classList.add("is-collapsed"); if (window.innerWidth > 768) ls.classList.remove("is-collapsed"); ls.style.setProperty("--sidebar-width", localStorage.getItem("sidebar-left-width"));`;
rightSidebarScript.innerHTML = `let rs = document.querySelector(".sidebar-right"); rs.classList.add("is-collapsed"); if (window.innerWidth > 768) rs.classList.remove("is-collapsed"); rs.style.setProperty("--sidebar-width", localStorage.getItem("sidebar-right-width"));`;
return {container: pageContainer, left: leftContent, leftBar: leftTopbarContent, right: rightContent, rightBar: rightTopbarContent, center: documentContainer};
}
private async addTitle()
{
if (!this.document || !this.sizerElement || this.viewType != "markdown") return;
// remove inline title
let inlineTitle = this.document.querySelector(".inline-title");
inlineTitle?.remove();
// remove make.md title
let makeTitle = this.document.querySelector(".mk-inline-context");
makeTitle?.remove();
// remove mod-header
let modHeader = this.document.querySelector(".mod-header");
modHeader?.remove();
// if the first header element is basically the same as the title, use it's text and remove it
let firstHeader = this.document.querySelector(":is(h1, h2, h3, h4, h5, h6):not(.markdown-embed-content *)");
if (firstHeader)
{
let firstHeaderText = (firstHeader.getAttribute("data-heading") ?? firstHeader.textContent)?.toLowerCase() ?? "";
let lowerTitle = this.title.toLowerCase();
let titleDiff = Utils.levenshteinDistance(firstHeaderText, lowerTitle) / lowerTitle.length;
let basenameDiff = Utils.levenshteinDistance(firstHeaderText, this.source.basename.toLowerCase()) / this.source.basename.length;
let difference = Math.min(titleDiff, basenameDiff);
if ((firstHeader.tagName == "H1" && difference < 0.2) || (firstHeader.tagName == "H2" && difference < 0.1))
{
if(this.titleInfo.isDefaultTitle)
{
firstHeader.querySelector(".heading-collapse-indicator")?.remove();
this.title = firstHeader.innerHTML;
ExportLog.log(`Using "${firstHeaderText}" header because it was very similar to the file's title.`);
}
else
{
ExportLog.log(`Replacing "${firstHeaderText}" header because it was very similar to the file's title.`);
}
firstHeader.remove();
}
else if (firstHeader.tagName == "H1")
{
// if the difference is too large but the first header is an h1 and it's the first element in the body, use it as the title
let headerEl = firstHeader.closest(".heading-wrapper") ?? firstHeader;
let headerParent = headerEl.parentElement;
if (headerParent && headerParent.classList.contains("markdown-preview-sizer"))
{
let childPosition = Array.from(headerParent.children).indexOf(headerEl);
if (childPosition <= 2)
{
if(this.titleInfo.isDefaultTitle)
{
firstHeader.querySelector(".heading-collapse-indicator")?.remove();
this.title = firstHeader.innerHTML;
ExportLog.log(`Using "${firstHeaderText}" header as title because it was H1 at the top of the page`);
}
else
{
ExportLog.log(`Replacing "${firstHeaderText}" header because it was H1 at the top of the page`);
}
firstHeader.remove();
}
}
}
}
// remove banner header
this.document.querySelector(".banner-header")?.remove();
// Create h1 inline title
let titleEl = this.document.createElement("h1");
titleEl.classList.add("page-title", "heading");
if (this.document?.body.classList.contains("show-inline-title")) titleEl.classList.add("inline-title");
titleEl.id = this.title;
let pageIcon = undefined;
// Create a div with icon
if ((this.icon != "" && !this.titleInfo.isDefaultIcon))
{
pageIcon = this.document.createElement("div");
pageIcon.id = "webpage-icon";
}
// Insert title into the title element
MarkdownRendererAPI.renderMarkdownSimpleEl(this.title, titleEl);
if (pageIcon)
{
MarkdownRendererAPI.renderMarkdownSimpleEl(this.icon, pageIcon);
titleEl.prepend(pageIcon);
}
// Insert title into the document
this.sizerElement.prepend(titleEl);
}
private async addHead()
{
if (!this.document) return;
let rootPath = this.pathToRoot.copy.makeWebStyle(this.exportOptions.webStylePaths).asString;
let description = this.description || (this.exportOptions.siteName + " - " + this.titleInfo.title);
let head =
`
<title>${this.titleInfo.title}</title>
<base href="${rootPath}/">
<meta id="root-path" root-path="${rootPath}/">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes, minimum-scale=1.0, maximum-scale=5.0">
<meta charset="UTF-8">
<meta name="description" content="${description}">
<meta property="og:title" content="${this.titleInfo.title}">
<meta property="og:description" content="${description}">
<meta property="og:type" content="website">
<meta property="og:url" content="${this.fullURL}">
<meta property="og:image" content="${this.metadataImageURL}">
<meta property="og:site_name" content="${this.exportOptions.siteName}">
`;
if (this.author && this.author != "")
{
head += `<meta name="author" content="${this.author}">`;
}
if (this.exportOptions.addRSS)
{
let rssURL = Path.joinStrings(this.exportOptions.siteURL ?? "", this.website?.rssPath ?? "").makeUnixStyle().asString;
head += `<link rel="alternate" type="application/rss+xml" title="RSS Feed" href="${rssURL}">`;
}
head += AssetHandler.getHeadReferences(this.exportOptions);
this.document.head.innerHTML = head;
}
private convertLinks()
{
if (!this.document) return;
this.document.querySelectorAll("a.internal-link").forEach((linkEl) =>
{
linkEl.setAttribute("target", "_self");
let href = linkEl.getAttribute("href");
if (!href) return;
if (href.startsWith("#")) // link pointing to header of this document
{
linkEl.setAttribute("href", href.replaceAll(" ", "_"));
}
else // if it doesn't start with #, it's a link to another document
{
let targetHeader = href.split("#").length > 1 ? "#" + href.split("#")[1] : "";
let target = href.split("#")[0];
let targetFile = app.metadataCache.getFirstLinkpathDest(target, this.source.path);
if (!targetFile) return;
let targetPath = new Path(targetFile.path);
if (MarkdownRendererAPI.isConvertable(targetPath.extensionName)) targetPath.setExtension("html");
targetPath.makeWebStyle(this.exportOptions.webStylePaths);
let finalHref = targetPath.makeUnixStyle() + targetHeader.replaceAll(" ", "_");
linkEl.setAttribute("href", finalHref);
}
});
this.document.querySelectorAll("a.footnote-link").forEach((linkEl) =>
{
linkEl.setAttribute("target", "_self");
});
this.document.querySelectorAll("h1, h2, h3, h4, h5, h6").forEach((headerEl) =>
{
// convert the data-heading to the id
headerEl.setAttribute("id", (headerEl.getAttribute("data-heading") ?? headerEl.textContent)?.replaceAll(" ", "_") ?? "");
});
}
private async inlineMedia()
{
if (!this.document) return;
let elements = Array.from(this.document.querySelectorAll("[src]:not(head [src])"))
for (let mediaEl of elements)
{
let rawSrc = mediaEl.getAttribute("src") ?? "";
let filePath = Webpage.getMediaPath(rawSrc, this.source.path);
if (filePath.isEmpty || filePath.isDirectory || filePath.isAbsolute) continue;
let base64 = await filePath.readFileString("base64") ?? "";
if (base64 === "") return;
let ext = filePath.extensionName;
//@ts-ignore
let type = app.viewRegistry.typeByExtension[ext] ?? "audio";
if(ext === "svg") ext += "+xml";
mediaEl.setAttribute("src", `data:${type}/${ext};base64,${base64}`);
};
}
private async exportMedia(): Promise<Downloadable[]>
{
if (!this.document) return [];
let downloads: Downloadable[] = [];
let elements = Array.from(this.document.querySelectorAll("[src]:not(head [src]):not(span)"));
for (let mediaEl of elements)
{
let rawSrc = mediaEl.getAttribute("src") ?? "";
let filePath = Webpage.getMediaPath(rawSrc, this.source.path);
if (filePath.isEmpty || filePath.isDirectory ||
filePath.isAbsolute || MarkdownRendererAPI.isConvertable(filePath.extension))
continue;
let exportLocation = filePath.copy;
// if the media is inside the exported folder then keep it in the same place
let sourceFolder = new Path(this.source.path).directory;
let mediaPathInExport = Path.getRelativePath(sourceFolder, filePath);
if (mediaPathInExport.asString.startsWith(".."))
{
// if path is outside of the vault, outline it into the media folder
exportLocation = AssetHandler.mediaPath.joinString(filePath.fullName);
}
exportLocation = exportLocation.makeWebStyle(this.exportOptions.webStylePaths).makeUnixStyle();
mediaEl.setAttribute("src", exportLocation.asString);
let data = await filePath.readFileBuffer();
if (data)
{
let imageDownload = new Downloadable(exportLocation.fullName, data, exportLocation.directory.makeForceFolder());
let imageStat = filePath.stat;
if (imageStat) imageDownload.modifiedTime = imageStat.mtimeMs;
downloads.push(imageDownload);
}
};
return downloads;
}
private static getMediaPath(src: string, exportingFilePath: string): Path
{
// @ts-ignore
let pathString = "";
if (src.startsWith("app://"))
{
let fail = false;
try
{
// @ts-ignore
pathString = app.vault.resolveFileUrl(src)?.path ?? "";
if (pathString == "") fail = true;
}
catch
{
fail = true;
}
if(fail)
{
pathString = src.replaceAll("app://", "").replaceAll("\\", "/");
pathString = pathString.replaceAll(pathString.split("/")[0] + "/", "");
pathString = Path.getRelativePathFromVault(new Path(pathString), true).asString;
ExportLog.log(pathString, "Fallback path parsing:");
}
}
else
{
pathString = app.metadataCache.getFirstLinkpathDest(src, exportingFilePath)?.path ?? "";
}
pathString = pathString ?? "";
return new Path(pathString);
}
}

View File

@ -1,526 +0,0 @@
import { Asset, AssetType, InlinePolicy, Mutability } from "scripts/html-generation/assets/asset";
import { Website } from "./website";
import Minisearch from 'minisearch';
import { ExportLog } from "scripts/html-generation/render-log";
import { Path } from "scripts/utils/path";
import { ExportPreset, Settings, SettingsPage } from "scripts/settings/settings";
import HTMLExportPlugin from "scripts/main";
import { TFile } from "obsidian";
import { AssetHandler } from "scripts/html-generation/asset-handler";
import { MarkdownRendererAPI } from "scripts/render-api";
import { MarkdownWebpageRendererAPIOptions } from "scripts/api-options";
export class WebsiteIndex
{
private web: Website;
public exportTime: number = Date.now();
public previousMetadata:
{
vaultName: string,
lastExport: number,
pluginVersion: string,
validBodyClasses: string,
useCustomHeadContent: boolean,
useCustomFavicon: boolean,
mainDependencies: string[],
files: string[],
fileInfo:
{
[path: string]:
{
modifiedTime: number,
sourceSize: number,
exportedPath: string,
dependencies: string[]
}
}
} | undefined = undefined;
public index: Minisearch<any> | undefined = undefined;
private stopWords = ["a", "about", "actually", "almost", "also", "although", "always", "am", "an", "and", "any", "are", "as", "at", "be", "became", "become", "but", "by", "can", "could", "did", "do", "does", "each", "either", "else", "for", "from", "had", "has", "have", "hence", "how", "i", "if", "in", "is", "it", "its", "just", "may", "maybe", "me", "might", "mine", "must", "my", "mine", "must", "my", "neither", "nor", "not", "of", "oh", "ok", "when", "where", "whereas", "wherever", "whenever", "whether", "which", "while", "who", "whom", "whoever", "whose", "why", "will", "with", "within", "without", "would", "yes", "yet", "you", "your"];
private indexOptions =
{
idField: 'path',
fields: ['path', 'title', 'content', 'tags', 'headers'],
storeFields: ['path', 'title', 'tags', 'headers'],
processTerm: (term:any, _fieldName:any) =>
this.stopWords.includes(term) ? null : term.toLowerCase()
}
// public exportOptions: MarkdownWebpageRendererAPIOptions = new MarkdownWebpageRendererAPIOptions();
private allFiles: string[] = []; // all files that are being exported
public removedFiles: string[] = []; // old files that are no longer being exported
public addedFiles: string[] = []; // new files that are being exported
private keptDependencies: string[] = []; // dependencies that are being kept
constructor(website: Website)
{
this.web = website;
}
public async init(): Promise<boolean>
{
this.exportTime = Date.now();
this.previousMetadata = await this.getExportMetadata();
this.index = await this.getExportIndex();
// Notify the user if all files will be exported
this.shouldApplyIncrementalExport(true);
if (!this.previousMetadata) return false;
return true;
}
public shouldApplyIncrementalExport(printWarning: boolean = false): boolean
{
let result = true;
if (!Settings.onlyExportModified) result = false;
if (Settings.exportPreset != ExportPreset.Website) result = false;
if (this.isVersionChanged() && this.previousMetadata)
{
if (printWarning) ExportLog.warning("Plugin version changed. All files will be re-exported.");
result = false;
}
if (this.previousMetadata == undefined)
{
if (printWarning) ExportLog.warning("No existing export metadata found. All files will be exported.");
result = false;
}
if (this.index == undefined)
{
if (printWarning) ExportLog.warning("No existing search index found. All files will be exported.");
result = false;
}
let rssAbsoultePath = this.web.destination.joinString(this.web.rssPath);
if (!rssAbsoultePath.exists && this.web.exportOptions.addRSS)
{
if (printWarning) ExportLog.warning("No existing RSS feed found. All files will be exported.");
result = false;
}
let customHeadChanged = this.previousMetadata && (this.previousMetadata?.useCustomHeadContent != (Settings.customHeadContentPath != ""));
if (customHeadChanged)
{
if (printWarning) ExportLog.warning(`${Settings.customHeadContentPath != "" ? "Added" : "Removed"} custom head content. All files will be re-exported.`);
result = false;
}
let customFaviconChanged = this.previousMetadata && (this.previousMetadata?.useCustomFavicon != (Settings.faviconPath != ""));
if (customFaviconChanged)
{
if (printWarning) ExportLog.warning(`${Settings.faviconPath != "" ? "Added" : "Removed"} custom favicon. All files will be re-exported.`);
result = false;
}
return result;
}
private async getExportMetadata(): Promise<any>
{
try
{
let metadataPath = this.web.destination.join(AssetHandler.libraryPath).joinString("metadata.json");
let metadata = await metadataPath.readFileString();
if (metadata) return JSON.parse(metadata);
}
catch (e)
{
ExportLog.warning(e, "Failed to parse metadata.json. Recreating metadata.");
}
return undefined;
}
private async getExportIndex(): Promise<Minisearch<any> | undefined>
{
let index: Minisearch<any> | undefined = undefined;
try
{
// load current index or create a new one if it doesn't exist
let indexPath = this.web.destination.join(AssetHandler.libraryPath).joinString("search-index.json");
let indexJson = await indexPath.readFileString();
if (indexJson)
{
index = Minisearch.loadJSON(indexJson, this.indexOptions);
}
}
catch (e)
{
ExportLog.warning(e, "Failed to load search-index.json. Creating new index.");
index = undefined;
}
return index;
}
public async createIndex(): Promise<Asset | undefined>
{
if (!this.index)
{
this.index = new Minisearch(this.indexOptions);
}
function preprocessContent(contentElement: HTMLElement): string
{
function getTextNodes(element: HTMLElement): Node[]
{
const textNodes = [];
const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null);
let node;
while (node = walker.nextNode()) {
textNodes.push(node);
}
return textNodes;
}
contentElement.querySelectorAll(".math, svg, img, .frontmatter, .metadata-container, .heading-after, style, script").forEach((el) => el.remove());
const textNodes = getTextNodes(contentElement);
let content = '';
for (const node of textNodes)
{
content += ' ' + node.textContent + ' ';
}
content = content.trim().replace(/\s+/g, ' ');
return content;
}
const htmlWebpages = this.web.webpages.filter(webpage => webpage.document && webpage.viewElement);
// progress counters
let progressCount = 0;
let totalCount = htmlWebpages.length + this.web.dependencies.length + this.removedFiles.length;
for (const webpage of htmlWebpages)
{
if(MarkdownRendererAPI.checkCancelled()) return undefined;
ExportLog.progress(progressCount, totalCount, "Indexing", "Adding: " + webpage.relativePath.asString, "var(--color-blue)");
const content = preprocessContent(webpage.viewElement);
if (content)
{
const webpagePath = webpage.relativePath.copy.makeUnixStyle().asString;
if (this.index.has(webpagePath))
{
this.index.discard(webpagePath);
}
this.index.add({
path: webpagePath,
title: webpage.title,
content: content,
tags: webpage.tags,
headers: webpage.headings.map((header) => header.heading),
});
}
else
{
console.warn(`No indexable content found for ${webpage.source.basename}`);
}
progressCount++;
}
// add other files to search
for (const file of this.web.dependencies)
{
if(MarkdownRendererAPI.checkCancelled()) return undefined;
const filePath = file.relativePath.asString;
if (this.index.has(filePath))
{
continue;
}
ExportLog.progress(progressCount, totalCount, "Indexing", "Adding: " + file.filename, "var(--color-blue)");
this.index.add({
path: filePath,
title: file.relativePath.basename,
content: "",
tags: [],
headers: [],
});
progressCount++;
}
// remove old files
for (const oldFile of this.removedFiles)
{
if(MarkdownRendererAPI.checkCancelled()) return undefined;
ExportLog.progress(progressCount, totalCount, "Indexing", "Removing: " + oldFile, "var(--color-blue)");
if (this.index.has(oldFile))
this.index.discard(oldFile);
progressCount++;
}
ExportLog.progress(totalCount, totalCount, "Indexing", "Cleanup index", "var(--color-blue)");
this.index.vacuum();
return new Asset("search-index.json", JSON.stringify(this.index), AssetType.Other, InlinePolicy.Download, false, Mutability.Temporary);
}
public async createMetadata(options: MarkdownWebpageRendererAPIOptions): Promise<Asset | undefined>
{
// metadata stores a list of files in the export, their relative paths, and modification times.
// is also stores the vault name, the export time, and the plugin version
let metadata: any = this.previousMetadata ? JSON.parse(JSON.stringify(this.previousMetadata)) : {};
metadata.vaultName = this.web.exportOptions.siteName;
metadata.lastExport = this.exportTime;
metadata.pluginVersion = HTMLExportPlugin.plugin.manifest.version;
metadata.validBodyClasses = Website.validBodyClasses;
metadata.useCustomHeadContent = Settings.customHeadContentPath != "";
metadata.useCustomFavicon = Settings.faviconPath != "";
metadata.files = this.allFiles;
metadata.mainDependencies = AssetHandler.getDownloads(options).map((asset) => asset.relativePath.copy.makeUnixStyle().asString);
if (!metadata.fileInfo) metadata.fileInfo = {};
// progress counters
let progressCount = 0;
let totalCount = this.web.webpages.length + this.web.dependencies.length + this.removedFiles.length;
for (const page of this.web.webpages)
{
if(MarkdownRendererAPI.checkCancelled()) return undefined;
ExportLog.progress(progressCount, totalCount, "Creating Metadata", "Adding: " + page.relativePath.asString, "var(--color-cyan)");
let fileInfo: any = {};
fileInfo.modifiedTime = this.exportTime;
fileInfo.sourceSize = page.source.stat.size;
fileInfo.exportedPath = page.relativePath.copy.makeUnixStyle().asString;
fileInfo.dependencies = page.dependencies.map((asset) => asset.relativePath.copy.makeUnixStyle().asString);
let exportPath = new Path(page.source.path).makeUnixStyle().asString;
metadata.fileInfo[exportPath] = fileInfo;
progressCount++;
}
for (const file of this.web.dependencies)
{
if(MarkdownRendererAPI.checkCancelled()) return undefined;
ExportLog.progress(progressCount, totalCount, "Creating Metadata", "Adding: " + file.relativePath.asString, "var(--color-cyan)");
let fileInfo: any = {};
fileInfo.modifiedTime = this.exportTime;
fileInfo.sourceSize = file.content.length;
fileInfo.exportedPath = file.relativePath.copy.makeUnixStyle().asString;
fileInfo.dependencies = [];
let exportPath = file.relativePath.copy.makeUnixStyle().asString;
metadata.fileInfo[exportPath] = fileInfo;
progressCount++;
}
// remove old files
for (const oldFile of this.removedFiles)
{
if(MarkdownRendererAPI.checkCancelled()) return undefined;
ExportLog.progress(progressCount, totalCount, "Creating Metadata", "Removing: " + oldFile, "var(--color-cyan)");
delete metadata.fileInfo[oldFile];
progressCount++;
}
return new Asset("metadata.json", JSON.stringify(metadata, null, 2), AssetType.Other, InlinePolicy.Download, false, Mutability.Temporary);
}
public async deleteOldFiles(options?: MarkdownWebpageRendererAPIOptions)
{
options = Object.assign(new MarkdownWebpageRendererAPIOptions(), options);
if (!this.previousMetadata) return;
if (this.removedFiles.length == 0)
{
ExportLog.log("No old files to delete");
return;
}
for (let i = 0; i < this.removedFiles.length; i++)
{
if(MarkdownRendererAPI.checkCancelled()) return;
let removedPath = this.removedFiles[i];
console.log("Removing old file: ", this.previousMetadata.fileInfo);
let exportedPath = new Path(this.previousMetadata.fileInfo[removedPath].exportedPath);
exportedPath.makeWebStyle(options.webStylePaths);
let deletePath = this.web.destination.join(exportedPath);
console.log("Deleting old file: " + deletePath.asString);
await deletePath.delete(true);
ExportLog.progress(i, this.removedFiles.length, "Deleting Old Files", "Deleting: " + deletePath.asString, "var(--color-orange)");
}
let folders = (await Path.getAllEmptyFoldersRecursive(this.web.destination));
if(MarkdownRendererAPI.checkCancelled()) return;
// sort by depth so that the deepest folders are deleted first
folders.sort((a, b) => a.depth - b.depth);
for (let i = 0; i < folders.length; i++)
{
if(MarkdownRendererAPI.checkCancelled()) return;
let folder = folders[i];
ExportLog.progress(i, folders.length, "Deleting Empty Folders", "Deleting: " + folder.asString, "var(--color-orange)");
await folder.directory.delete(true);
}
}
public async updateBodyClasses()
{
if (!this.previousMetadata) return;
if (this.previousMetadata.validBodyClasses == Website.validBodyClasses) return;
console.log("Updating body classes of previous export");
let convertableFiles = this.previousMetadata.files.filter((path) => MarkdownRendererAPI.isConvertable(path.split(".").pop() ?? ""));
let exportedPaths = convertableFiles.map((path) => new Path(this.previousMetadata?.fileInfo[path]?.exportedPath ?? "", this.web.destination.asString));
exportedPaths = exportedPaths.filter((path) => !path.isEmpty);
for (let i = 0; i < exportedPaths.length; i++)
{
if(MarkdownRendererAPI.checkCancelled()) return;
let exportedPath = exportedPaths[i];
let content = await exportedPath.readFileString();
if (!content) continue;
let dom = new DOMParser().parseFromString(content, "text/html");
let body = dom.querySelector("body");
if (!body) continue;
body.className = Website.validBodyClasses;
await exportedPath.writeFile(dom.documentElement.outerHTML);
ExportLog.progress(i, exportedPaths.length, "Updating Body Classes", "Updating: " + exportedPath.asString, "var(--color-yellow)");
}
}
public isFileChanged(file: TFile): boolean
{
let metadata = this.getMetadataForFile(file);
if (!metadata)
{
return true;
}
return metadata.modifiedTime < file.stat.mtime || metadata.sourceSize !== file.stat.size;
}
public hasFile(file: TFile): boolean
{
return this.getMetadataForFile(file) !== undefined;
}
public hasFileByPath(path: string): boolean
{
return this.getMetadataForPath(path) !== undefined;
}
public getMetadataForFile(file: TFile): {modifiedTime: number,sourceSize: number,exportedPath: string,dependencies: string[]} | undefined
{
return this.previousMetadata?.fileInfo[file.path];
}
public getMetadataForPath(path: string): {modifiedTime: number,sourceSize: number,exportedPath: string,dependencies: string[]} | undefined
{
return this.previousMetadata?.fileInfo[path];
}
public isVersionChanged(): boolean
{
return this.previousMetadata?.pluginVersion !== HTMLExportPlugin.plugin.manifest.version;
}
public getAllFiles(): string[]
{
this.allFiles = [];
for (let file of this.web.batchFiles)
{
this.allFiles.push(file.path);
}
for (let asset of this.web.dependencies)
{
if (this.allFiles.some((path) => Path.equal(path, asset.relativePath.asString))) continue;
this.allFiles.push(asset.relativePath.copy.makeUnixStyle().asString);
}
return this.allFiles;
}
public getRemovedFiles(): string[]
{
if (!this.previousMetadata) return [];
this.removedFiles = this.previousMetadata.files.filter((path) =>
{
return !this.allFiles.includes(path) &&
!this.previousMetadata?.mainDependencies.includes(path) &&
!this.keptDependencies.includes(path);
});
console.log("Old files: ", this.removedFiles);
return this.removedFiles;
}
public getAddedFiles(): string[]
{
if (!this.previousMetadata) return [];
this.addedFiles = this.allFiles.filter(path => !this.previousMetadata?.files.includes(path));
console.log("New files: ", this.addedFiles);
return this.addedFiles;
}
public getKeptDependencies(): string[]
{
if (!this.previousMetadata) return [];
this.keptDependencies = [];
for (let file of this.allFiles)
{
let dep = this.previousMetadata.fileInfo[file]?.dependencies ?? [];
this.keptDependencies.push(...dep);
}
// add kept dependencies to the list of all files and remove duplicates
this.allFiles.push(...this.keptDependencies);
this.allFiles = this.allFiles.filter((path, index) => this.allFiles.findIndex((f) => f == path) == index);
return this.keptDependencies;
}
public async build(options: MarkdownWebpageRendererAPIOptions): Promise<boolean>
{
this.getAllFiles();
this.getKeptDependencies();
this.getRemovedFiles();
this.getAddedFiles();
// create website metadata and index
let metadataAsset = await this.createMetadata(options);
if (!metadataAsset) return false;
this.web.dependencies.push(metadataAsset);
this.web.downloads.push(metadataAsset);
if (options.addSearch) // only create index if search bar is enabled
{
let index = await this.createIndex();
if (!index) return false;
this.web.dependencies.push(index);
this.web.downloads.push(index);
}
return true;
}
}

View File

@ -1,507 +0,0 @@
import { Downloadable } from "scripts/utils/downloadable";
import { Webpage } from "./webpage";
import { FileTree } from "./file-tree";
import { AssetHandler } from "scripts/html-generation/asset-handler";
import { TAbstractFile, TFile, TFolder } from "obsidian";
import { Settings } from "scripts/settings/settings";
import { GraphView } from "./graph-view";
import { Path } from "scripts/utils/path";
import { ExportLog } from "scripts/html-generation/render-log";
import { Asset, AssetType, InlinePolicy, Mutability } from "scripts/html-generation/assets/asset";
import HTMLExportPlugin from "scripts/main";
import { WebsiteIndex } from "./website-index";
import { HTMLGeneration } from "scripts/html-generation/html-generation-helpers";
import { MarkdownRendererAPI } from "scripts/render-api";
import { MarkdownWebpageRendererAPIOptions } from "scripts/api-options";
import RSS from 'rss';
export class Website
{
public webpages: Webpage[] = [];
public dependencies: Downloadable[] = [];
public downloads: Downloadable[] = [];
public batchFiles: TFile[] = [];
public progress: number = 0;
public destination: Path;
public index: WebsiteIndex;
public rss: RSS;
public rssPath = AssetHandler.libraryPath.joinString("rss.xml").makeUnixStyle().asString;
private globalGraph: GraphView;
private fileTree: FileTree;
private fileTreeHtml: string = "";
public graphDataAsset: Asset;
public fileTreeAsset: Asset;
public static validBodyClasses: string;
public exportOptions: MarkdownWebpageRendererAPIOptions;
/**
* Create a new website with the given files and options.
* @param files The files to include in the website.
* @param destination The folder to export the website to.
* @param options The api options to use for the export.
* @returns The website object.
*/
public async createWithFiles(files: TFile[], destination: Path, options?: MarkdownWebpageRendererAPIOptions): Promise<Website | undefined>
{
this.exportOptions = Object.assign(new MarkdownWebpageRendererAPIOptions(), options);
this.batchFiles = files;
this.destination = destination;
await this.initExport();
console.log("Creating website with files: ", files);
let useIncrementalExport = this.index.shouldApplyIncrementalExport();
for (let file of files)
{
if(MarkdownRendererAPI.checkCancelled()) return;
ExportLog.progress(this.progress, this.batchFiles.length, "Generating HTML", "Exporting: " + file.path, "var(--interactive-accent)");
this.progress++;
let filename = new Path(file.path).basename;
let webpage = new Webpage(file, destination, filename, this, this.exportOptions);
let shouldExportPage = (useIncrementalExport && this.index.isFileChanged(file)) || !useIncrementalExport;
if (!shouldExportPage) continue;
let createdPage = await webpage.create();
if(!createdPage) continue;
this.webpages.push(webpage);
this.downloads.push(webpage);
this.downloads.push(...webpage.dependencies);
this.dependencies.push(...webpage.dependencies);
}
this.dependencies.push(...AssetHandler.getDownloads(this.exportOptions));
this.downloads.push(...AssetHandler.getDownloads(this.exportOptions));
this.filterDownloads(true);
this.index.build(this.exportOptions);
this.filterDownloads();
if (this.exportOptions.addRSS)
{
this.createRSS();
}
console.log("Website created: ", this);
return this;
}
private giveWarnings()
{
// if iconize plugin is installed, warn if note icons are not enabled
// @ts-ignore
if (app.plugins.enabledPlugins.has("obsidian-icon-folder"))
{
// @ts-ignore
let fileToIconName = app.plugins.plugins['obsidian-icon-folder'].data;
let noteIconsEnabled = fileToIconName.settings.iconsInNotesEnabled ?? false;
if (!noteIconsEnabled)
{
ExportLog.warning("For Iconize plugin support, enable \"Toggle icons while editing notes\" in the Iconize plugin settings.");
}
}
// if excalidraw installed and the embed mode is not set to Native SVG, warn
// @ts-ignore
if (app.plugins.enabledPlugins.has("obsidian-excalidraw-plugin"))
{
// @ts-ignore
let embedMode = app.plugins.plugins['obsidian-excalidraw-plugin']?.settings['previewImageType'] ?? "";
if (embedMode != "SVG")
{
ExportLog.warning("For Excalidraw embed support, set the embed mode to \"Native SVG\" in the Excalidraw plugin settings.");
}
}
// the plugin only supports the banner plugin above version 2.0.5
// @ts-ignore
if (app.plugins.enabledPlugins.has("obsidian-banners"))
{
// @ts-ignore
let bannerPlugin = app.plugins.plugins['obsidian-banners'];
let version = bannerPlugin?.manifest?.version ?? "0.0.0";
version = version.substring(0, 5);
if (version < "2.0.5")
{
ExportLog.warning("The Banner plugin version 2.0.5 or higher is required for full support. You have version " + version + ".");
}
}
// warn the user if they are trying to create an rss feed without a site url
if (this.exportOptions.addRSS && (this.exportOptions.siteURL == "" || this.exportOptions.siteURL == undefined))
{
ExportLog.warning("Creating an RSS feed requires a site url to be set in the export settings.");
}
}
private async initExport()
{
this.progress = 0;
this.index = new WebsiteIndex(this);
await MarkdownRendererAPI.beginBatch();
this.giveWarnings();
if (this.exportOptions.addGraphView)
{
ExportLog.progress(0, 1, "Initialize Export", "Generating graph view", "var(--color-yellow)");
let convertableFiles = this.batchFiles.filter((file) => MarkdownRendererAPI.isConvertable(file.extension));
this.globalGraph = new GraphView();
await this.globalGraph.init(convertableFiles, this.exportOptions);
}
if (this.exportOptions.addFileNavigation)
{
ExportLog.progress(0, 1, "Initialize Export", "Generating file tree", "var(--color-yellow)");
this.fileTree = new FileTree(this.batchFiles, false, true);
this.fileTree.makeLinksWebStyle = this.exportOptions.webStylePaths ?? true;
this.fileTree.showNestingIndicator = true;
this.fileTree.generateWithItemsClosed = true;
this.fileTree.showFileExtentionTags = true;
this.fileTree.hideFileExtentionTags = ["md"]
this.fileTree.title = this.exportOptions.siteName ?? app.vault.getName();
this.fileTree.class = "file-tree";
let tempTreeContainer = document.body.createDiv();
await this.fileTree.generateTreeWithContainer(tempTreeContainer);
this.fileTreeHtml = tempTreeContainer.innerHTML;
tempTreeContainer.remove();
}
// wipe all temporary assets and reload dynamic assets
ExportLog.progress(0, 1, "Initialize Export", "loading assets", "var(--color-yellow)");
await AssetHandler.reloadAssets();
Website.validBodyClasses = await HTMLGeneration.getValidBodyClasses(true);
if (this.exportOptions.addGraphView)
{
ExportLog.progress(1, 1, "Loading graph asset", "...", "var(--color-yellow)");
this.graphDataAsset = new Asset("graph-data.js", this.globalGraph.getExportData(), AssetType.Script, InlinePolicy.AutoHead, true, Mutability.Temporary);
this.graphDataAsset.load(this.exportOptions);
}
if (this.exportOptions.addFileNavigation)
{
ExportLog.progress(1, 1, "Loading file tree asset", "...", "var(--color-yellow)");
this.fileTreeAsset = new Asset("file-tree.html", this.fileTreeHtml, AssetType.HTML, InlinePolicy.Auto, true, Mutability.Temporary);
this.fileTreeAsset.load(this.exportOptions);
}
ExportLog.progress(1, 1, "Initializing index", "...", "var(--color-yellow)");
await this.index.init();
}
private async createRSS()
{
let author = this.exportOptions.authorName || undefined;
this.rss = new RSS(
{
title: this.exportOptions.siteName ?? app.vault.getName(),
description: "Obsidian digital garden",
generator: "Webpage HTML Export plugin for Obsidian",
feed_url: Path.joinStrings(this.exportOptions.siteURL ?? "", this.rssPath).asString,
site_url: this.exportOptions.siteURL ?? "",
image_url: Path.joinStrings(this.exportOptions.siteURL ?? "", AssetHandler.favicon.relativePath.asString).asString,
pubDate: new Date(this.index.exportTime),
copyright: author,
ttl: 60,
custom_elements:
[
{ "dc:creator": author },
]
});
for (let page of this.webpages)
{
// only include convertable pages with content
if (!page.isConvertable || page.sizerElement.innerText.length < 5) continue;
let title = page.title;
let url = Path.joinStrings(this.exportOptions.siteURL ?? "", page.relativePath.asString).asString;
let guid = page.source.path;
let date = new Date(page.source.stat.mtime);
author = page.author ?? author;
let media = page.metadataImageURL ?? "";
let hasMedia = media != "";
let description = page.description;
if (!description)
{
let content = page.viewElement.cloneNode(true) as HTMLElement;
content.querySelectorAll(`h1, h2, h3, h4, h5, h6, .mermaid, table, mjx-container, style, script,
.mod-header, .mod-footer, .metadata-container, .frontmatter, img[src^="data:"]`).forEach((heading) => heading.remove());
// update image links
content.querySelectorAll("[src]").forEach((el: HTMLImageElement) =>
{
let src = el.src;
if (!src) return;
if (src.startsWith("http") || src.startsWith("data:")) return;
src = src.replace("app://obsidian", "");
src = src.replace(".md", "");
let path = Path.joinStrings(this.exportOptions.siteURL ?? "", src);
el.src = path.asString;
});
// update normal links
content.querySelectorAll("[href]").forEach((el: HTMLAnchorElement) =>
{
let href = el.href;
if (!href) return;
if (href.startsWith("http") || href.startsWith("data:")) return;
href = href.replace("app://obsidian", "");
href = href.replace(".md", "");
let path = Path.joinStrings(this.exportOptions.siteURL ?? "", href);
el.href = path.asString;
});
// console.log("Content: ", content.outerHTML);
function keepTextLinksImages(element: HTMLElement)
{
let walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT);
let node;
let nodes = [];
while (node = walker.nextNode())
{
if (node.nodeType == Node.ELEMENT_NODE)
{
let element = node as HTMLElement;
if (element.tagName == "A" || element.tagName == "IMG" || element.tagName == "BR")
{
nodes.push(element);
}
if (element.tagName == "DIV")
{
let classes = element.parentElement?.classList;
if (classes?.contains("heading-children") || classes?.contains("markdown-preview-sizer"))
{
nodes.push(document.createElement("br"));
}
}
if (element.tagName == "LI")
{
nodes.push(document.createElement("br"));
}
}
else
{
if (node.parentElement?.tagName != "A" && node.parentElement?.tagName != "IMG")
nodes.push(node);
}
}
element.innerHTML = "";
element.append(...nodes);
}
keepTextLinksImages(content);
description = content.innerHTML;
content.remove();
}
// add tags to top of description
let tags = page.tags.map((t) => t).map((tag) => `<a class="tag" href="${this.exportOptions.siteURL}?query=tag:${tag.replace("#", "")}">${tag}</a>`).join(" ");
let tagContainer = document.body.createDiv();
tagContainer.innerHTML = tags;
tagContainer.style.display = "flex";
tagContainer.style.gap = "0.4em";
tagContainer.querySelectorAll("a.tag").forEach((tag: HTMLElement) =>
{
tag.style.backgroundColor = "#046c74";
tag.style.color = "white";
tag.style.fontWeight = "700";
tag.style.border = "none";
tag.style.borderRadius = "1em";
tag.style.padding = "0.2em 0.5em";
});
description = tagContainer.innerHTML + " \n " + description;
tagContainer.remove();
this.rss.item(
{
title: title,
description: description,
url: url,
guid: guid,
date: date,
enclosure: hasMedia ? { url: media } : undefined,
author: author,
custom_elements:
[
hasMedia ? { "content:encoded": `<figure><img src="${media}"></figure>` } : undefined,
]
});
}
let result = this.rss.xml();
let rssAbsoultePath = this.destination.joinString(this.rssPath);
let rssFileOld = await rssAbsoultePath.readFileString();
if (rssFileOld)
{
let rssDocOld = new DOMParser().parseFromString(rssFileOld, "text/xml");
let rssDocNew = new DOMParser().parseFromString(result, "text/xml");
// insert old items into new rss and remove duplicates
let oldItems = Array.from(rssDocOld.querySelectorAll("item")) as HTMLElement[];
let newItems = Array.from(rssDocNew.querySelectorAll("item")) as HTMLElement[];
oldItems = oldItems.filter((oldItem) => !newItems.find((newItem) => newItem.querySelector("guid")?.textContent == oldItem.querySelector("guid")?.textContent));
oldItems = oldItems.filter((oldItem) => !this.index.removedFiles.contains(oldItem.querySelector("guid")?.textContent ?? ""));
newItems = newItems.concat(oldItems);
// remove all items from new rss
newItems.forEach((item) => item.remove());
// add items back to new rss
let channel = rssDocNew.querySelector("channel");
newItems.forEach((item) => channel?.appendChild(item));
result = rssDocNew.documentElement.outerHTML;
}
let rss = new Asset("rss.xml", result, AssetType.Other, InlinePolicy.Download, false, Mutability.Temporary);
rss.download(this.destination);
}
private filterDownloads(onlyDuplicates: boolean = false)
{
// remove duplicates from the dependencies and downloads
this.dependencies = this.dependencies.filter((file, index) => this.dependencies.findIndex((f) => f.relativePath.asString == file.relativePath.asString) == index);
this.downloads = this.downloads.filter((file, index) => this.downloads.findIndex((f) => f.relativePath.asString == file.relativePath.asString) == index);
// remove files that have not been modified since last export
if (!this.index.shouldApplyIncrementalExport() || onlyDuplicates) return;
let localThis = this;
function filterFunction(file: Downloadable)
{
// always include .html files
if (file.filename.endsWith(".html")) return true;
// always exclude fonts if they exist
if
(
localThis.index.hasFileByPath(file.relativePath.asString) &&
file.filename.endsWith(".woff") ||
file.filename.endsWith(".woff2") ||
file.filename.endsWith(".otf") ||
file.filename.endsWith(".ttf")
)
{
return false;
}
// always include files that have been modified since last export
let metadata = localThis.index.getMetadataForPath(file.relativePath.copy.makeUnixStyle().asString);
if (metadata && (file.modifiedTime > metadata.modifiedTime || metadata.sourceSize != file.content.length))
return true;
console.log("Excluding: " + file.relativePath.asString);
return false;
}
this.dependencies = this.dependencies.filter(filterFunction);
this.downloads = this.downloads.filter(filterFunction);
}
// TODO: Seperate the icon and title into seperate functions
public static async getTitleAndIcon(file: TAbstractFile, skipIcon:boolean = false): Promise<{ title: string; icon: string; isDefaultIcon: boolean; isDefaultTitle: boolean }>
{
const { app } = HTMLExportPlugin.plugin;
const { titleProperty } = Settings;
let iconOutput = "";
let iconProperty: string | undefined = "";
let title = file.name;
let isDefaultTitle = true;
let useDefaultIcon = false;
if (file instanceof TFile)
{
const fileCache = app.metadataCache.getFileCache(file);
const frontmatter = fileCache?.frontmatter;
title = (frontmatter?.[titleProperty] ?? frontmatter?.banner_header)?.toString() ?? file.basename;
if (title != file.basename) isDefaultTitle = false;
if (title.endsWith(".excalidraw")) title = title.substring(0, title.length - 11);
iconProperty = frontmatter?.icon ?? frontmatter?.sticker ?? frontmatter?.banner_icon; // banner plugin support
if (!iconProperty && Settings.showDefaultTreeIcons)
{
useDefaultIcon = true;
let isMedia = Asset.extentionToType(file.extension) == AssetType.Media;
iconProperty = isMedia ? Settings.defaultMediaIcon : Settings.defaultFileIcon;
if (file.extension == "canvas") iconProperty = "lucide//layout-dashboard";
}
}
if (skipIcon) return { title: title, icon: "", isDefaultIcon: true, isDefaultTitle: isDefaultTitle };
if (file instanceof TFolder && Settings.showDefaultTreeIcons)
{
iconProperty = Settings.defaultFolderIcon;
useDefaultIcon = true;
}
iconOutput = await HTMLGeneration.getIcon(iconProperty ?? "");
// add iconize icon as frontmatter if iconize exists
let isUnchangedNotEmojiNotHTML = (iconProperty == iconOutput && iconOutput.length < 40) && !/\p{Emoji}/u.test(iconOutput) && !iconOutput.includes("<") && !iconOutput.includes(">");
let parsedAsIconize = false;
//@ts-ignore
if ((useDefaultIcon || !iconProperty || isUnchangedNotEmojiNotHTML) && app.plugins.enabledPlugins.has("obsidian-icon-folder"))
{
//@ts-ignore
let fileToIconName = app.plugins.plugins['obsidian-icon-folder'].data;
let noteIconsEnabled = fileToIconName.settings.iconsInNotesEnabled ?? false;
// only add icon if rendering note icons is enabled
// because that is what we rely on to get the icon
if (noteIconsEnabled)
{
let iconIdentifier = fileToIconName.settings.iconIdentifier ?? ":";
let iconProperty = fileToIconName[file.path];
if (iconProperty && typeof iconProperty != "string")
{
iconProperty = iconProperty.iconName ?? "";
}
if (iconProperty && typeof iconProperty == "string" && iconProperty.trim() != "")
{
if (file instanceof TFile)
app.fileManager.processFrontMatter(file, (frontmatter) =>
{
frontmatter.icon = iconProperty;
});
iconOutput = iconIdentifier + iconProperty + iconIdentifier;
parsedAsIconize = true;
}
}
}
if (!parsedAsIconize && isUnchangedNotEmojiNotHTML) iconOutput = "";
return { title: title, icon: iconOutput, isDefaultIcon: useDefaultIcon, isDefaultTitle: isDefaultTitle };
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,271 +0,0 @@
import { ButtonComponent, Modal, Setting, TFile } from 'obsidian';
import { Utils } from '../utils/utils';
import HTMLExportPlugin from '../main';
import { ExportPreset, Settings, SettingsPage } from './settings';
import { FilePickerTree } from '../objects/file-picker';
import { Path } from 'scripts/utils/path';
export interface ExportInfo
{
canceled: boolean;
pickedFiles: TFile[];
exportPath: Path;
validPath: boolean;
}
export class ExportModal extends Modal
{
private isClosed: boolean = true;
private canceled: boolean = true;
private filePickerModalEl: HTMLElement;
private filePicker: FilePickerTree;
private pickedFiles: TFile[] | undefined = undefined;
private validPath: boolean = true;
public static title: string = "Export to HTML";
public exportInfo: ExportInfo;
constructor() {
super(app);
}
overridePickedFiles(files: TFile[])
{
this.pickedFiles = files;
}
/**
* @brief Opens the modal and async blocks until the modal is closed.
* @returns True if the EXPORT button was pressed, false is the export was canceled.
* @override
*/
async open(): Promise<ExportInfo>
{
this.isClosed = false;
this.canceled = true;
super.open();
if(!this.filePickerModalEl)
{
this.filePickerModalEl = this.containerEl.createDiv({ cls: 'modal' });
this.containerEl.insertBefore(this.filePickerModalEl, this.modalEl);
this.filePickerModalEl.style.position = 'relative';
this.filePickerModalEl.style.zIndex = "1";
this.filePickerModalEl.style.width = "25em";
this.filePickerModalEl.style.padding = "0";
this.filePickerModalEl.style.margin = "10px";
this.filePickerModalEl.style.maxHeight = "80%";
this.filePickerModalEl.style.boxShadow = "0 0 7px 1px inset #00000060";
let container = this.filePickerModalEl.createDiv({ cls: 'modal-content tree-container mod-root file-picker-tree file-tree mod-nav-indicator' });
container.style.height = "100%";
container.style.width = "100%";
container.style.padding = "0";
container.style.margin = "0";
container.style.display = "flex";
container.style.flexDirection = "column";
container.style.alignItems = "flex-end";
let scrollArea = container.createDiv({ cls: 'tree-scroll-area' });
scrollArea.style.height = "100%";
scrollArea.style.width = "100%";
scrollArea.style.overflowY = "auto";
scrollArea.style.overflowX = "hidden";
scrollArea.style.padding = "1em";
scrollArea.style.boxShadow = "0 0 7px 1px inset #00000060";
this.filePicker = new FilePickerTree(app.vault.getFiles(), true, true);
this.filePicker.generateWithItemsClosed = true;
this.filePicker.showFileExtentionTags = true;
this.filePicker.hideFileExtentionTags = ["md"];
await this.filePicker.generateTree(scrollArea);
if((this.pickedFiles?.length ?? 0 > 0) || Settings.filesToExport[0].length > 0)
{
let filesToPick = this.pickedFiles?.map(file => file.path) ?? Settings.filesToExport[0];
this.filePicker.setSelectedFiles(filesToPick);
}
let saveFiles = new Setting(container).addButton((button) =>
{
button.setButtonText("Save").onClick(async () =>
{
Settings.filesToExport[0] = this.filePicker.getSelectedFilesSavePaths();
await SettingsPage.saveSettings();
});
});
saveFiles.settingEl.style.border = "none";
saveFiles.settingEl.style.marginRight = "1em";
}
const { contentEl } = this;
contentEl.empty();
this.titleEl.setText(ExportModal.title);
if (HTMLExportPlugin.updateInfo.updateAvailable)
{
// create red notice showing the update is available
let updateNotice = contentEl.createEl('strong', { text: `Update Available: ${HTMLExportPlugin.updateInfo.currentVersion}${HTMLExportPlugin.updateInfo.latestVersion}` });
updateNotice.setAttribute("style",
`margin-block-start: calc(var(--h3-size)/2);
background-color: var(--interactive-normal);
padding: 4px;
padding-left: 1em;
padding-right: 1em;
color: var(--color-red);
border-radius: 5px;
display: block;
width: fit-content;`)
// create normal block with update notes
let updateNotes = contentEl.createEl('div', { text: HTMLExportPlugin.updateInfo.updateNote });
updateNotes.setAttribute("style",
`margin-block-start: calc(var(--h3-size)/2);
background-color: var(--background-secondary-alt);
padding: 4px;
padding-left: 1em;
padding-right: 1em;
color: var(--text-normal);
font-size: var(--font-ui-smaller);
border-radius: 5px;
display: block;
width: fit-content;
white-space: pre-wrap;`)
}
let modeDescriptions =
{
"website": "This will export a file structure suitable for uploading to your own web server.",
"documents": "This will export self-contained, but slow loading and large, html documents.",
"raw-documents": "This will export raw, self-contained documents without the website layout. This is useful for sharing individual notes, or printing."
}
let exportModeSetting = new Setting(contentEl)
.setName('Export Mode')
// @ts-ignore
.setDesc(modeDescriptions[Settings.exportPreset] + "\n\nSome options are only available in certain modes.")
.setHeading()
.addDropdown((dropdown) => dropdown
.addOption('website', 'Online Web Server')
.addOption('documents', 'HTML Documents')
.addOption('raw-documents', 'Raw HTML Documents')
.setValue(["website", "documents", "raw-documents"].contains(Settings.exportPreset) ? Settings.exportPreset : 'website')
.onChange(async (value) =>
{
Settings.exportPreset = value as ExportPreset;
switch (value) {
case 'website':
Settings.inlineAssets = false;
Settings.makeNamesWebStyle = true;
Settings.addGraphView = true;
Settings.addFileNav = true;
Settings.addSearchBar = true;
await SettingsPage.saveSettings();
break;
case 'documents':
Settings.inlineAssets = true;
Settings.makeNamesWebStyle = false;
Settings.addFileNav = true;
Settings.addGraphView = false;
Settings.addSearchBar = false;
await SettingsPage.saveSettings();
break;
case 'raw-documents':
Settings.inlineAssets = true;
Settings.makeNamesWebStyle = false;
Settings.addGraphView = false;
Settings.addFileNav = false;
Settings.addSearchBar = false;
await SettingsPage.saveSettings();
break;
}
this.open();
}
));
exportModeSetting.descEl.style.whiteSpace = "pre-wrap";
SettingsPage.createToggle(contentEl, "Open after export", () => Settings.openAfterExport, (value) => Settings.openAfterExport = value);
let exportButton : ButtonComponent | undefined = undefined;
function setExportDisabled(disabled: boolean)
{
if(exportButton)
{
exportButton.setDisabled(disabled);
if (exportButton.disabled) exportButton.buttonEl.style.opacity = "0.5";
else exportButton.buttonEl.style.opacity = "1";
}
}
let validatePath = (path: Path) => path.validate(
{
allowEmpty: false,
allowRelative: false,
allowAbsolute: true,
allowDirectories: true,
allowTildeHomeDirectory: true,
requireExists: true
});
let exportPathInput = SettingsPage.createFileInput(contentEl, () => Settings.exportPath, (value) => Settings.exportPath = value,
{
name: '',
description: '',
placeholder: 'Type or browse an export directory...',
defaultPath: Utils.idealDefaultPath(),
pickFolder: true,
validation: validatePath,
onChanged: (path) => (!validatePath(path).valid) ? setExportDisabled(true) : setExportDisabled(false)
});
let { fileInput } = exportPathInput;
fileInput.addButton((button) => {
exportButton = button;
setExportDisabled(!this.validPath);
button.setButtonText('Export').onClick(async () =>
{
this.canceled = false;
this.close();
});
});
new Setting(contentEl)
.setDesc("More options located on the plugin settings page.")
.addExtraButton((button) => button.setTooltip('Open plugin settings').onClick(() => {
//@ts-ignore
app.setting.open();
//@ts-ignore
app.setting.openTabById('webpage-html-export');
}));
this.filePickerModalEl.style.height = this.modalEl.clientHeight * 2 + "px";
await Utils.waitUntil(() => this.isClosed, 60 * 60 * 1000, 10);
this.pickedFiles = this.filePicker.getSelectedFiles();
this.filePickerModalEl.remove();
this.exportInfo = { canceled: this.canceled, pickedFiles: this.pickedFiles, exportPath: new Path(Settings.exportPath), validPath: this.validPath};
return this.exportInfo;
}
onClose()
{
const { contentEl } = this;
contentEl.empty();
this.isClosed = true;
ExportModal.title = "Export to HTML";
}
}

View File

@ -1,50 +0,0 @@
export class FlowList {
containerEl: HTMLElement;
flowListEl: HTMLElement;
checkedList: string[] = [];
constructor(containerEl: HTMLElement) {
this.containerEl = containerEl;
this.flowListEl = this.containerEl.createDiv({ cls: 'flow-list' });
}
addItem(name: string, key: string, value: boolean, onChange: (value: boolean) => void): HTMLElement {
let item = this.flowListEl.createDiv({ cls: 'flow-item' });
let checkbox = item.createEl('input', { type: 'checkbox' });
checkbox.checked = value;
if (checkbox.checked) this.checkedList.push(key)
item.addEventListener('click', (evt) =>
{
if (!checkbox.checked)
{
checkbox.checked = true;
if (!this.checkedList.includes(key))
this.checkedList.push(key)
}
else
{
checkbox.checked = false;
if (this.checkedList.includes(key))
this.checkedList.remove(key)
}
onChange(checkbox.checked);
});
// override the default checkbox behavior
checkbox.onclick = (evt) =>
{
checkbox.checked = !checkbox.checked;
}
let label = item.createDiv({ cls: 'flow-label' });
label.setText(name);
return item;
}
}

View File

@ -1,50 +0,0 @@
import HTMLExportPlugin from "scripts/main"
import { DEFAULT_SETTINGS, Settings, SettingsPage } from "./settings"
import { ExportLog } from "scripts/html-generation/render-log";
import { Notice } from "obsidian";
export async function migrateSettings()
{
if (Settings.settingsVersion == HTMLExportPlugin.pluginVersion) return;
new Notice("Webpage HTML Export settings have been updated to a new version. Please update your settings if any have been reset.", 10000);
var settingsToSave =
[
"filesToExport",
"exportPath",
"includePluginCSS",
"includeGraphView",
"graphMaxNodeSize",
"graphMinNodeSize",
"graphEdgePruning",
"graphCentralForce",
"graphRepulsionForce",
"graphLinkLength",
"graphAttractionForce",
]
try
{
var savedSettings = JSON.parse(JSON.stringify(Object.assign({}, Settings)));
Object.assign(Settings, DEFAULT_SETTINGS);
for (var i = 0; i < settingsToSave.length; i++)
{
var settingName = settingsToSave[i];
// @ts-ignore
Settings[settingName] = savedSettings[settingName];
}
Settings.settingsVersion = HTMLExportPlugin.pluginVersion;
}
catch (e)
{
ExportLog.error(e, "Failed to migrate settings, resetting to default settings.");
Object.assign(Settings, DEFAULT_SETTINGS);
}
await SettingsPage.saveSettings();
return;
}

View File

@ -1,862 +0,0 @@
import { Notice, Plugin, PluginSettingTab, Setting, TFile, TFolder, TextComponent, Vault, getIcon } from 'obsidian';
import { Utils } from '../utils/utils';
import { Path } from '../utils/path';
import pluginStylesBlacklist from 'assets/third-party-styles-blacklist.txt';
import { FlowList } from './flow-list';
import { ExportInfo, ExportModal } from './export-modal';
import { migrateSettings } from './settings-migration';
import { ExportLog } from 'scripts/html-generation/render-log';
// #region Settings Definition
export enum ExportPreset
{
Website = "website",
Documents = "documents",
RawDocuments = "raw-documents",
}
export enum EmojiStyle
{
Native = "Native",
Twemoji = "Twemoji",
OpenMoji = "OpenMoji",
OpenMojiOutline = "OpenMojiOutline",
FluentUI = "FluentUI",
}
export class Settings
{
public static settingsVersion: string;
// Asset Options
public static makeOfflineCompatible: boolean;
public static inlineAssets: boolean;
public static includePluginCSS: string;
public static includeSvelteCSS: boolean;
public static titleProperty: string;
public static customHeadContentPath: string;
public static faviconPath: string;
// Layout Options
public static documentWidth: string;
public static sidebarWidth: string;
// Behavior Options
public static minOutlineCollapse: number;
public static startOutlineCollapsed: boolean;
public static allowFoldingHeadings: boolean;
public static allowFoldingLists: boolean;
public static allowResizingSidebars: boolean;
// Export Options
public static logLevel: "all" | "warning" | "error" | "fatal" | "none";
public static minifyHTML: boolean;
public static makeNamesWebStyle: boolean;
public static onlyExportModified: boolean;
public static deleteOldFiles: boolean;
// Page Features
public static addThemeToggle: boolean;
public static addOutline: boolean;
public static addFileNav: boolean;
public static addSearchBar: boolean;
public static addGraphView: boolean;
public static addTitle: boolean;
public static addRSSFeed: boolean;
// Main Export Options
public static siteURL: string;
public static authorName: string;
public static vaultTitle: string;
public static exportPreset: ExportPreset;
public static openAfterExport: boolean;
// Graph View Settings
public static graphAttractionForce: number;
public static graphLinkLength: number;
public static graphRepulsionForce: number;
public static graphCentralForce: number;
public static graphEdgePruning: number;
public static graphMinNodeSize: number;
public static graphMaxNodeSize: number;
// icons
public static showDefaultTreeIcons: boolean;
public static emojiStyle: EmojiStyle;
public static defaultFileIcon: string;
public static defaultFolderIcon: string;
public static defaultMediaIcon: string;
// Cache
public static exportPath: string;
public static filesToExport: string[][];
}
export const DEFAULT_SETTINGS: Settings =
{
settingsVersion: "0.0.0",
// Asset Options
makeOfflineCompatible: false,
inlineAssets: false,
includePluginCSS: '',
includeSvelteCSS: true,
titleProperty: 'title',
customHeadContentPath: '',
faviconPath: '',
// Layout Options
documentWidth: "40em",
sidebarWidth: "20em",
// Behavior Options
minOutlineCollapse: 2,
startOutlineCollapsed: false,
allowFoldingHeadings: true,
allowFoldingLists: true,
allowResizingSidebars: true,
// Export Options
logLevel: "warning",
minifyHTML: true,
makeNamesWebStyle: true,
onlyExportModified: true,
deleteOldFiles: true,
// Page Features
addThemeToggle: true,
addOutline: true,
addFileNav: true,
addSearchBar: true,
addGraphView: true,
addTitle: true,
addRSSFeed: true,
// Main Export Options
siteURL: '',
authorName: '',
vaultTitle: app.vault.getName(),
exportPreset: ExportPreset.Website,
openAfterExport: false,
// Graph View Settings
graphAttractionForce: 1,
graphLinkLength: 10,
graphRepulsionForce: 150,
graphCentralForce: 3,
graphEdgePruning: 100,
graphMinNodeSize: 3,
graphMaxNodeSize: 7,
// icons
showDefaultTreeIcons: false,
emojiStyle: EmojiStyle.Native,
defaultFileIcon: "lucide//file",
defaultFolderIcon: "lucide//folder",
defaultMediaIcon: "lucide//file-image",
// Cache
exportPath: '',
filesToExport: [[]],
}
// #endregion
export class SettingsPage extends PluginSettingTab
{
display()
{
const { containerEl: contentEl } = this;
// #region Settings Header
contentEl.empty();
let header = contentEl.createEl('h2', { text: 'HTML Export Settings' });
header.style.display = 'block';
header.style.marginBottom = '15px';
let supportContainer = contentEl.createDiv();
supportContainer.style.marginBottom = '15px';
let supportLink = contentEl.createEl('a');
let buttonColor = Utils.sampleCSSColorHex("--color-accent", document.body).hex;
let buttonTextColor = Utils.sampleCSSColorHex("--text-on-accent", document.body).hex;
// @ts-ignore
supportLink.href = `href="https://www.buymeacoffee.com/nathangeorge"`;
supportLink.style.height = "40px"
supportLink.innerHTML = `<img style="height:40px;" src="https://img.buymeacoffee.com/button-api/?text=Buy me a coffee&emoji=&slug=nathangeorge&button_colour=${buttonColor}&font_colour=${buttonTextColor}&font_family=Poppins&outline_colour=${buttonTextColor}&coffee_colour=FFDD00">`;
let supportHeader = contentEl.createDiv({ text: 'Support the continued development of this plugin.', cls: "setting-item-description" });
supportHeader.style.display = 'block';
supportContainer.style.display = 'grid';
supportContainer.style.gridTemplateColumns = "0.5fr 0.5fr";
supportContainer.style.gridTemplateRows = "40px 20px";
supportContainer.appendChild(supportLink);
// debug info button
let debugInfoButton = contentEl.createEl('button');
let bugIcon = getIcon('bug');
if (bugIcon) debugInfoButton.appendChild(bugIcon);
debugInfoButton.style.height = '100%';
debugInfoButton.style.aspectRatio = '1/1';
debugInfoButton.style.justifySelf = 'end';
let debugHeader = contentEl.createDiv({ text: 'Copy debug info to clipboard', cls: "setting-item-description" });
debugHeader.style.display = 'block';
debugHeader.style.justifySelf = 'end';
debugInfoButton.addEventListener('click', () => {
navigator.clipboard.writeText(ExportLog.getDebugInfo());
new Notice("Debug info copied to clipboard!");
});
supportContainer.appendChild(debugInfoButton);
supportContainer.appendChild(supportHeader);
supportContainer.appendChild(debugHeader);
// #endregion
//#region Page Features
if (Settings.exportPreset != ExportPreset.RawDocuments)
{
SettingsPage.createDivider(contentEl);
let section = SettingsPage.createSection(contentEl, 'Page Features', 'Control the visibility of different page features');
SettingsPage.createToggle(section, 'Theme toggle', () => Settings.addThemeToggle, (value) => Settings.addThemeToggle = value);
SettingsPage.createToggle(section, 'Document outline / table of contents', () => Settings.addOutline, (value) => Settings.addOutline = value);
SettingsPage.createToggle(section, 'File navigation tree', () => Settings.addFileNav, (value) => Settings.addFileNav = value);
SettingsPage.createToggle(section, 'File & folder icons', () => Settings.showDefaultTreeIcons, (value) => Settings.showDefaultTreeIcons = value);
if (Settings.exportPreset == ExportPreset.Website)
{
SettingsPage.createToggle(section, 'Search bar', () => Settings.addSearchBar, (value) => Settings.addSearchBar = value);
SettingsPage.createToggle(section, 'Graph view', () => Settings.addGraphView, (value) => Settings.addGraphView = value);
let graphViewSection = SettingsPage.createSection(section, 'Graph View Settings', 'Control the behavior of the graph view simulation');
new Setting(graphViewSection)
.setName('Attraction Force')
.setDesc("How much should linked nodes attract each other? This will make the graph appear more clustered.")
.addSlider((slider) => slider
.setLimits(0, 100, 1)
.setValue(Settings.graphAttractionForce / (2 / 100))
.setDynamicTooltip()
.onChange(async (value) => {
// remap to 0 - 2;
let remapMultiplier = 2 / 100;
Settings.graphAttractionForce = value * remapMultiplier;
await SettingsPage.saveSettings();
})
.showTooltip()
);
new Setting(graphViewSection)
.setName('Link Length')
.setDesc("How long should the links between nodes be? The shorter the links the closer connected nodes will cluster together.")
.addSlider((slider) => slider
.setLimits(0, 100, 1)
.setValue(Settings.graphLinkLength)
.setDynamicTooltip()
.onChange(async (value) => {
Settings.graphLinkLength = value;
await SettingsPage.saveSettings();
})
.showTooltip()
);
new Setting(graphViewSection)
.setName('Repulsion Force')
.setDesc("How much should nodes repel each other? This will make the graph appear more spread out.")
.addSlider((slider) => slider
.setLimits(0, 100, 1)
.setValue(Settings.graphRepulsionForce / 3)
.setDynamicTooltip()
.onChange(async (value) => {
Settings.graphRepulsionForce = value * 3;
await SettingsPage.saveSettings();
})
.showTooltip()
);
new Setting(graphViewSection)
.setName('Central Force')
.setDesc("How much should nodes be attracted to the center? This will make the graph appear more dense and circular.")
.addSlider((slider) => slider
.setLimits(0, 100, 1)
.setValue(Settings.graphCentralForce / (5 / 100))
.setDynamicTooltip()
.onChange(async (value) => {
// remap to 0 - 5;
let remapMultiplier = 5 / 100;
Settings.graphCentralForce = value * remapMultiplier;
await SettingsPage.saveSettings();
})
.showTooltip()
);
new Setting(graphViewSection)
.setName('Max Node Radius')
.setDesc("How large should the largest nodes be? Nodes are sized by how many links they have. The larger a node is the more it will attract other nodes. This can be used to create a good grouping around the most important nodes.")
.addSlider((slider) => slider
.setLimits(3, 15, 1)
.setValue(Settings.graphMaxNodeSize)
.setDynamicTooltip()
.onChange(async (value) => {
Settings.graphMaxNodeSize = value;
await SettingsPage.saveSettings();
})
.showTooltip()
);
new Setting(graphViewSection)
.setName('Min Node Radius')
.setDesc("How small should the smallest nodes be? The smaller a node is the less it will attract other nodes.")
.addSlider((slider) => slider
.setLimits(3, 15, 1)
.setValue(Settings.graphMinNodeSize)
.setDynamicTooltip()
.onChange(async (value) => {
Settings.graphMinNodeSize = value;
await SettingsPage.saveSettings();
})
.showTooltip()
);
new Setting(graphViewSection)
.setName('Edge Pruning Factor')
.setDesc("Edges with a length above this threshold will not be rendered, however they will still contribute to the simulation. This can help large tangled graphs look more organised. Hovering over a node will still display these links.")
.addSlider((slider) => slider
.setLimits(0, 100, 1)
.setValue(100 - Settings.graphEdgePruning)
.setDynamicTooltip()
.onChange(async (value) => {
Settings.graphEdgePruning = 100 - value;
await SettingsPage.saveSettings();
})
.showTooltip()
);
}
let iconTutorial = new Setting(section)
.setName('Custom icons')
.setDesc(
`Use the 'Iconize' plugin to add custom icons to your files and folders.
Or set the 'icon' property of your file to an emoji or lucide icon name.
This feature does not require "File & folder icons" to be enbaled.`);
iconTutorial.infoEl.style.whiteSpace = "pre-wrap";
new Setting(section)
.setName('Icon emoji style')
.addDropdown((dropdown) =>
{
for (let style in EmojiStyle) dropdown.addOption(style, style);
dropdown.setValue(Settings.emojiStyle);
dropdown.onChange(async (value) => {
Settings.emojiStyle = value as EmojiStyle;
await SettingsPage.saveSettings();
});
});
SettingsPage.createText(section, 'Page title property', () => Settings.titleProperty, (value) => Settings.titleProperty = value,
"Override a specific file's title / name by defining this property in the frontmatter.");
SettingsPage.createFileInput(section, () => Settings.customHeadContentPath, (value) => Settings.customHeadContentPath = value,
{
name: 'Custom head content',
description: 'Custom scripts, styles, or anything else (html file)',
placeholder: 'Path to html formatted file...',
defaultPath: Path.vaultPath,
validation: (path) => path.validate(
{
allowEmpty: true,
allowAbsolute: true,
allowRelative: true,
allowFiles: true,
requireExists: true,
requireExtentions: ["html, htm, txt"]
}),
});
SettingsPage.createFileInput(section, () => Settings.faviconPath, (value) => Settings.faviconPath = value,
{
name: 'Favicon path',
description: 'Add a custom favicon image to the website',
placeholder: 'Path to image file...',
defaultPath: Path.vaultPath,
validation: (path) => path.validate(
{
allowEmpty: true,
allowAbsolute: true,
allowRelative: true,
allowFiles: true,
requireExists: true,
requireExtentions: ["png", "ico", "jpg", "jpeg", "svg"]
}),
});
}
//#endregion
//#region Page Behaviors
let section;
if (Settings.exportPreset != ExportPreset.RawDocuments)
{
SettingsPage.createDivider(contentEl);
section = SettingsPage.createSection(contentEl, 'Page Behaviors', 'Change the behavior of included page features');
new Setting(section)
.setName('Min Outline Collapse Depth')
.setDesc('Only allow outline items to be collapsed if they are at least this many levels deep in the tree.')
.addDropdown((dropdown) => dropdown.addOption('1', '1').addOption('2', '2').addOption('100', 'No Collapse')
.setValue(Settings.minOutlineCollapse.toString())
.onChange(async (value) => {
Settings.minOutlineCollapse = parseInt(value);
await SettingsPage.saveSettings();
}));
SettingsPage.createToggle(section, 'Start Outline Collapsed', () => Settings.startOutlineCollapsed, (value) => Settings.startOutlineCollapsed = value,
'All outline items will be collapsed by default.');
SettingsPage.createToggle(section, 'Allow folding headings', () => Settings.allowFoldingHeadings, (value) => Settings.allowFoldingHeadings = value,
'Fold headings using an arrow icon, like in Obsidian.');
SettingsPage.createToggle(section, 'Allow folding lists', () => Settings.allowFoldingLists, (value) => Settings.allowFoldingLists = value,
'Fold lists using an arrow icon, like in Obsidian.');
SettingsPage.createToggle(section, 'Allow resizing sidebars', () => Settings.allowResizingSidebars, (value) => Settings.allowResizingSidebars = value,
'Allow the user to resize the sidebar width.');
}
//#endregion
//#region Layout Options
SettingsPage.createDivider(contentEl);
section = SettingsPage.createSection(contentEl, 'Layout Options', 'Set document and sidebar widths');
new Setting(section)
.setName('Document Width')
.setDesc('Sets the line width of the exported document in css units. (ex. 600px, 50em)')
.addText((text) => text
.setValue(Settings.documentWidth)
.setPlaceholder('40em')
.onChange(async (value) => {
Settings.documentWidth = value;
await SettingsPage.saveSettings();
}
))
.addExtraButton((button) => button.setIcon('reset').setTooltip('Reset to default').onClick(() => {
Settings.documentWidth = "";
SettingsPage.saveSettings();
this.display();
}));
new Setting(section)
.setName('Sidebar Width')
.setDesc('Sets the width of the sidebar in css units. (ex. 20em, 200px)')
.addText((text) => text
.setValue(Settings.sidebarWidth)
.setPlaceholder('20em')
.onChange(async (value) => {
Settings.sidebarWidth = value;
await SettingsPage.saveSettings();
}
))
.addExtraButton((button) => button.setIcon('reset').setTooltip('Reset to default').onClick(() => {
Settings.sidebarWidth = "";
SettingsPage.saveSettings();
this.display();
}));
//#endregion
//#region Export Options
SettingsPage.createDivider(contentEl);
section = SettingsPage.createSection(contentEl, 'Export Options', 'Change the behavior of the export process.');
SettingsPage.createToggle(section, 'Only export modfied files', () => Settings.onlyExportModified, (value) => Settings.onlyExportModified = value,
'Only generate new html for files which have been modified since the last export.');
SettingsPage.createToggle(section, 'Delete old files', () => Settings.deleteOldFiles, (value) => Settings.deleteOldFiles = value,
'Delete files from a previous export that are no longer being exported.');
SettingsPage.createToggle(section, 'Minify HTML', () => Settings.minifyHTML, (value) => Settings.minifyHTML = value,
'Minify HTML to make it load faster.');
new Setting(section)
.setName('Log Level')
.setDesc('Set the level of logging to display in the export log.')
.addDropdown((dropdown) => dropdown
.addOption('all', 'All')
.addOption('warning', 'Warning')
.addOption('error', 'Error')
.addOption('fatal', 'Only Fatal Errors')
.setValue(Settings.logLevel)
.onChange(async (value: "all" | "warning" | "error" | "fatal" | "none") =>
{
Settings.logLevel = value;
await SettingsPage.saveSettings();
}));
//#endregion
//#region Asset Settings
SettingsPage.createDivider(contentEl);
section = SettingsPage.createSection(contentEl, 'Asset Options', 'Add plugin styles, or make the page offline compatible.');
SettingsPage.createToggle(section, 'Make Offline Compatible', () => Settings.makeOfflineCompatible, (value) => Settings.makeOfflineCompatible = value,
'Download any online assets / images / scripts so the page can be viewed offline. Or so the website does not depend on a CDN.');
SettingsPage.createToggle(section, 'Include Svelte CSS', () => Settings.includeSvelteCSS, (value) => Settings.includeSvelteCSS = value,
'Include the CSS from any plugins that use the svelte framework. These can not be chosen individually because their styles are not associated with their respective plugins.');
new Setting(section)
.setName('Include CSS from Plugins')
.setDesc('Include the CSS from the following plugins in the exported HTML. If plugin features aren\'t rendering correctly, try adding the plugin to this list. Avoid adding plugins unless you specifically notice a problem, because more CSS will increase the loading time of your page.')
let pluginsList = new FlowList(section);
Utils.getPluginIDs().forEach(async (plugin) => {
let pluginManifest = Utils.getPluginManifest(plugin);
if (!pluginManifest) return;
if ((await this.getBlacklistedPluginIDs()).contains(pluginManifest.id)) {
return;
}
let pluginDir = pluginManifest.dir;
if (!pluginDir) return;
let pluginPath = new Path(pluginDir);
let hasCSS = pluginPath.joinString('styles.css').exists;
if (!hasCSS) return;
let isChecked = Settings.includePluginCSS.match(new RegExp(`^${plugin}`, 'm')) != null;
pluginsList.addItem(pluginManifest.name, plugin, isChecked, (value) => {
Settings.includePluginCSS = pluginsList.checkedList.join('\n');
SettingsPage.saveSettings();
});
});
//#endregion
//#region Advanced
SettingsPage.createDivider(contentEl);
section = SettingsPage.createSection(contentEl, 'Metadata', 'Control general site data and RSS feed creation');
SettingsPage.createText(section, 'Public site URL', () => Settings.siteURL, (value) => Settings.siteURL = ((value.endsWith("/") || value == "") ? value : value + "/").trim(),
'The url that this site will be hosted at. This is needed to reference links and images in metadata and RSS. (Because these links cannot be relative)',
(value) => (value.startsWith("http://") || value.startsWith("https://") || value.trim() == "") ? "" : "URL must start with 'http://' or 'https://'");
SettingsPage.createText(section, 'Author Name', () => Settings.authorName, (value) => Settings.authorName = value,
'The default name of the author of the site');
SettingsPage.createText(section, 'Vault Title', () => Settings.vaultTitle, (value) => Settings.vaultTitle = value,
'The title of the vault');
SettingsPage.createToggle(section, 'Create RSS feed', () => Settings.addRSSFeed, (value) => Settings.addRSSFeed = value,
`Create an RSS feed for the website located at ${Settings.siteURL}lib/rss.xml`);
let summaryTutorial = new Setting(section)
.setName('Metadata Properties')
.setDesc(
`Use the 'description' or 'summary' property to set a custom summary of a page.
Use the 'author' property to set the author of a specific page.`);
summaryTutorial.infoEl.style.whiteSpace = "pre-wrap";
//#endregion
//#region Experimental
// if (Settings.exportPreset == ExportPreset.Website)
// {
// let experimentalContainer = contentEl.createDiv();
// let experimentalHR1 = experimentalContainer.createEl('hr');
// let experimentalHeader = experimentalContainer.createEl('span', { text: 'Experimental' });
// let experimentalHR2 = experimentalContainer.createEl('hr');
// experimentalContainer.style.display = 'flex';
// experimentalContainer.style.marginTop = '5em';
// experimentalContainer.style.alignItems = 'center';
// experimentalHR1.style.borderColor = "var(--color-red)";
// experimentalHR2.style.borderColor = "var(--color-red)";
// experimentalHeader.style.color = "var(--color-red)";
// experimentalHR1.style.flexGrow = "1";
// experimentalHR2.style.flexGrow = "1";
// experimentalHeader.style.flexGrow = "0.1";
// experimentalHeader.style.textAlign = "center";
// let experimentalHREnd = contentEl.createEl('hr');
// experimentalHREnd.style.borderColor = "var(--color-red)";
// }
//#endregion
}
// #region Class Functions and Variables
static settings: Settings = DEFAULT_SETTINGS;
static plugin: Plugin;
static loaded = false;
private blacklistedPluginIDs: string[] = [];
public async getBlacklistedPluginIDs(): Promise<string[]>
{
if (this.blacklistedPluginIDs.length > 0) return this.blacklistedPluginIDs;
this.blacklistedPluginIDs = pluginStylesBlacklist.replaceAll("\r", "").split("\n");
return this.blacklistedPluginIDs;
}
constructor(plugin: Plugin) {
super(app, plugin);
SettingsPage.plugin = plugin;
}
static async loadSettings()
{
let loadedSettings = await SettingsPage.plugin.loadData();
Object.assign(Settings, DEFAULT_SETTINGS, loadedSettings);
await migrateSettings();
SettingsPage.loaded = true;
}
static async saveSettings() {
await SettingsPage.plugin.saveData(Object.assign({}, Settings));
}
static renameFile(file: TFile, oldPath: string)
{
let oldPathParsed = new Path(oldPath).asString;
Settings.filesToExport.forEach((fileList) =>
{
let index = fileList.indexOf(oldPathParsed);
if (index >= 0)
{
fileList[index] = file.path;
}
});
SettingsPage.saveSettings();
}
static async updateSettings(usePreviousSettings: boolean = false, overrideFiles: TFile[] | undefined = undefined, overrideExportPath: Path | undefined = undefined): Promise<ExportInfo | undefined>
{
if (!usePreviousSettings)
{
let modal = new ExportModal();
if(overrideFiles) modal.overridePickedFiles(overrideFiles);
return await modal.open();
}
let files = Settings.filesToExport[0];
let path = overrideExportPath ?? new Path(Settings.exportPath);
if ((files.length == 0 && overrideFiles == undefined) || !path.exists || !path.isAbsolute || !path.isDirectory)
{
new Notice("Please set the export path and files to export in the settings first.", 5000);
let modal = new ExportModal();
if(overrideFiles) modal.overridePickedFiles(overrideFiles);
return await modal.open();
}
return undefined;
}
static getFilesToExport(): TFile[]
{
let files: TFile[] = [];
let allFiles = app.vault.getFiles();
let exportPaths = Settings.filesToExport[0];
if (!exportPaths) return [];
for (let path of exportPaths)
{
let file = app.vault.getAbstractFileByPath(path);
if (file instanceof TFile) files.push(file);
else if (file instanceof TFolder)
{
let newFiles = allFiles.filter((f) => f.path.startsWith(file?.path ?? "*"));
files.push(...newFiles);
}
};
return files;
}
public static createDivider(container: HTMLElement)
{
let hr = container.createEl("hr");
hr.style.marginTop = "20px";
hr.style.marginBottom = "20px";
hr.style.borderColor = "var(--interactive-accent)";
hr.style.opacity = "0.5";
}
public static createToggle(container: HTMLElement, name: string, get: () => boolean, set: (value: boolean) => void, desc: string = ""): Setting
{
let setting = new Setting(container);
setting.setName(name)
if (desc != "") setting.setDesc(desc);
setting.addToggle((toggle) => toggle
// @ts-ignore
.setValue(get())
.onChange(async (value) => {
// @ts-ignore
set(value);
await SettingsPage.saveSettings();
}));
return setting;
}
public static createText(container: HTMLElement, name: string, get: () => string, set: (value: string) => void, desc: string = "", validation?: (value: string) => string): Setting
{
let setting = new Setting(container);
let errorText = this.createError(container);
let value = get();
if (value != "") errorText.setText(validation ? validation(value) : "");
setting.setName(name)
if (desc != "") setting.setDesc(desc);
setting.addText((text) => text
.setValue(value)
.onChange(async (value) =>
{
let error = validation ? validation(value) : "";
if (error == "")
{
set(value);
await SettingsPage.saveSettings();
}
errorText.setText(error);
}));
return setting;
}
public static createError(container: HTMLElement): HTMLElement
{
let error = container.createDiv({ cls: 'setting-item-description' });
error.style.color = "var(--color-red)";
error.style.marginBottom = "0.75rem";
return error;
}
public static createFileInput(container: HTMLElement, get: () => string, set: (value: string) => void, options?: {name?: string, description?: string, placeholder?: string, defaultPath?: Path, pickFolder?: boolean, validation?: (path: Path) => {valid: boolean, isEmpty: boolean, error: string}, browseButton?: boolean, onChanged?: (path: Path)=>void}): {fileInput: Setting, textInput: TextComponent, browseButton: HTMLElement | undefined}
{
let name = options?.name ?? "";
let description = options?.description ?? "";
let placeholder = options?.placeholder ?? "Path to file...";
let defaultPath = options?.defaultPath ?? Path.vaultPath;
let pickFolder = options?.pickFolder ?? false;
let validation = options?.validation ?? ((path) => ({valid: true, isEmpty: false, error: ""}));
let browseButton = options?.browseButton ?? true;
let onChanged = options?.onChanged;
let headContentErrorMessage = this.createError(container);
if (get().trim() != "")
{
let tempPath = new Path(get());
headContentErrorMessage.setText(validation(tempPath).error);
}
let headContentInput : TextComponent | undefined = undefined;
let fileInput = new Setting(container);
if(name != "") fileInput.setName(name);
if (description != "") fileInput.setDesc(description);
if (name == "" && description == "") fileInput.infoEl.style.display = "none";
let textEl: TextComponent;
fileInput.addText((text) =>
{
textEl = text;
headContentInput = text;
text.inputEl.style.width = '100%';
text.setPlaceholder(placeholder)
.setValue(get())
.onChange(async (value) =>
{
let path = new Path(value);
let valid = validation(path);
headContentErrorMessage.setText(valid.error);
if (valid.valid)
{
headContentErrorMessage.setText("");
set(value.replaceAll("\"", ""));
text.setValue(get());
await SettingsPage.saveSettings();
}
if (onChanged) onChanged(path);
});
});
let browseButtonEl = undefined;
if(browseButton)
{
fileInput.addButton((button) =>
{
browseButtonEl = button.buttonEl;
button.setButtonText('Browse').onClick(async () =>
{
let path = pickFolder ? await Utils.showSelectFolderDialog(defaultPath) : await Utils.showSelectFileDialog(defaultPath);
if (!path) return;
set(path.asString);
let valid = validation(path);
headContentErrorMessage.setText(valid.error);
if (valid.valid)
{
await SettingsPage.saveSettings();
}
if (onChanged) onChanged(path);
headContentInput?.setValue(get());
});
});
}
container.appendChild(headContentErrorMessage);
return {fileInput: fileInput, textInput: textEl!, browseButton: browseButtonEl};
}
public static createSection(container: HTMLElement, name: string, desc: string): HTMLElement
{
let section = container.createEl('details');
let summary = section.createEl('summary');
summary.style.display = "block";
summary.style.marginLeft = "-1em";
section.style.paddingLeft = "2em";
section.style.borderLeft = "1px solid var(--interactive-accent)";
new Setting(summary)
.setName(name)
.setDesc(desc)
.setHeading()
return section;
}
// #endregion
}

View File

@ -1,50 +0,0 @@
import { Path } from "./path";
export class Downloadable
{
/**
* The name of the file with the extention
*/
public filename: string;
/**
* The raw data of the file
*/
public content: string | Buffer;
public relativeDirectory: Path;
public encoding: BufferEncoding | undefined;
public modifiedTime: number = 0; // when was the source file last modified
constructor(filename: string, content: string | Buffer, vaultRelativeDestination: Path, encoding: BufferEncoding | undefined = "utf8")
{
if(vaultRelativeDestination.isFile) throw new Error("vaultRelativeDestination must be a folder: " + vaultRelativeDestination.asString);
this.filename = filename;
this.content = content;
this.relativeDirectory = vaultRelativeDestination;
this.encoding = encoding;
}
public get relativePath(): Path
{
return this.relativeDirectory.joinString(this.filename);
}
async download(downloadDirectory: Path)
{
let data = this.content instanceof Buffer ? this.content : Buffer.from(this.content.toString(), this.encoding);
let writePath = this.getAbsoluteDownloadDirectory(downloadDirectory).joinString(this.filename);
await writePath.writeFile(data, this.encoding);
}
public getAbsoluteDownloadPath(downloadDirectory: Path): Path
{
return this.relativeDirectory.absolute(downloadDirectory).joinString(this.filename);
}
public getAbsoluteDownloadDirectory(downloadDirectory: Path): Path
{
return this.relativeDirectory.absolute(downloadDirectory);
}
}

View File

@ -1,872 +0,0 @@
const pathTools = require('upath');
import { Stats, existsSync } from 'fs';
import { FileSystemAdapter, Notice } from 'obsidian';
import { Utils } from './utils';
import { promises as fs } from 'fs';
import { statSync } from 'fs';
import internal from 'stream';
import { ExportLog } from 'scripts/html-generation/render-log';
import { join } from 'path';
import { homedir } from 'os';
export class Path
{
private static logQueue: { title: string, message: any, type: "info" | "warn" | "error" | "fatal" }[] = [];
private static log(title: string, message: any, type: "info" | "warn" | "error" | "fatal")
{
this.logQueue.push({ title: title, message: message, type: type });
}
public static dequeueLog(): { title: string, message: any, type: "info" | "warn" | "error" | "fatal" }[]
{
let queue = this.logQueue;
this.logQueue = [];
return queue;
}
private _root: string = "";
private _dir: string = "";
private _parent: string = "";
private _base: string = "";
private _ext: string = "";
private _name: string = "";
private _fullPath: string = "";
private _isDirectory: boolean = false;
private _isFile: boolean = false;
private _exists: boolean | undefined = undefined;
private _workingDirectory: string;
private _rawString: string = "";
private _isWindows: boolean = process.platform === "win32";
constructor(path: string, workingDirectory: string = Path.vaultPath.asString)
{
this._workingDirectory = Path.parsePath(workingDirectory).fullPath;
this.reparse(path);
if (this.isAbsolute) this._workingDirectory = "";
}
reparse(path: string): Path
{
let parsed = Path.parsePath(path);
this._root = parsed.root;
this._dir = parsed.dir;
this._parent = parsed.parent;
this._base = parsed.base;
this._ext = parsed.ext;
this._name = parsed.name;
this._fullPath = parsed.fullPath;
this._isDirectory = this._ext == "";
this._isFile = this._ext != "";
this._exists = undefined;
this._rawString = path;
if (this._isWindows)
{
if (this._root.startsWith("http:") || this._root.startsWith("https:"))
{
this._isWindows = false;
this.reparse(this._fullPath.replaceAll("\\", "/"));
}
else
{
this._root = this._root.replaceAll("/", "\\");
this._dir = this._dir.replaceAll("/", "\\");
this._parent = this._parent.replaceAll("/", "\\");
this._fullPath = this._fullPath.replaceAll("/", "\\");
this._workingDirectory = this._workingDirectory.replaceAll("/", "\\");
}
}
this._exists; // force a re-evaluation of the exists property which will also throw an error if the path does not exist
return this;
}
joinString(...paths: string[]): Path
{
return this.copy.reparse(Path.joinStringPaths(this.asString, ...paths));
}
join(...paths: Path[]): Path
{
return new Path(Path.joinStringPaths(this.asString, ...paths.map(p => p.asString)), this._workingDirectory);
}
makeAbsolute(workingDirectory: string | Path = this._workingDirectory): Path
{
if(workingDirectory instanceof Path && !workingDirectory.isAbsolute) throw new Error("workingDirectory must be an absolute path: " + workingDirectory.asString);
if (!this.isAbsolute)
{
this._fullPath = Path.joinStringPaths(workingDirectory.toString(), this.asString);
this._workingDirectory = "";
this.reparse(this.asString);
}
return this;
}
makeForceFolder(): Path
{
if (!this.isDirectory)
{
this.reparse(this.asString + "/");
}
return this;
}
makeNormalized(): Path
{
let fullPath = pathTools.normalizeSafe(this.absolute().asString);
let newWorkingDir = "";
let newFullPath = "";
let reachedEndOfWorkingDir = false;
for (let i = 0; i < fullPath.length; i++)
{
let fullChar = fullPath.charAt(i);
let workingChar = this.workingDirectory.charAt(i);
if (fullChar == workingChar && !reachedEndOfWorkingDir)
{
newWorkingDir += fullChar;
continue;
}
reachedEndOfWorkingDir = true;
newFullPath += fullChar;
}
this.reparse(newFullPath);
this._workingDirectory = newWorkingDir;
return this;
}
normalized(): Path
{
return this.copy.makeNormalized();
}
makeRootAbsolute(): Path
{
if (!this.isAbsolute)
{
if (this._isWindows)
{
if(this._fullPath.contains(":"))
{
this._fullPath = this.asString.substring(this._fullPath.indexOf(":") - 1);
}
else
{
this._fullPath = "\\" + this.asString;
}
}
else
{
this._fullPath = "/" + this.asString;
}
this.reparse(this.asString);
}
return this;
}
setWorkingDirectory(workingDirectory: string): Path
{
this._workingDirectory = workingDirectory;
return this;
}
makeRootRelative(): Path
{
if (this.isAbsolute)
{
if (this._isWindows)
{
// replace the drive letter and colon with nothing
this._fullPath = this.asString.replace(/^.:\/\//i, "").replace(/^.:\//i, "");
this._fullPath = Utils.trimStart(this._fullPath, "\\");
}
else
{
this._fullPath = Utils.trimStart(this._fullPath, "/");
}
this.reparse(this.asString);
}
return this;
}
makeWebStyle(makeWebStyle: boolean = true): Path
{
if (!makeWebStyle) return this;
this._fullPath = Path.toWebStyle(this.asString);
this.reparse(this.asString);
return this;
}
makeWindowsStyle(): Path
{
this._isWindows = true;
this._fullPath = this.asString.replaceAll("/", "\\");
this.reparse(this.asString);
return this;
}
makeUnixStyle(): Path
{
this._isWindows = false;
this._fullPath = this.asString.replaceAll("\\", "/").replace(/^.:\/\//i, "/");
this.reparse(this.asString);
return this;
}
setExtension(extension: string): Path
{
if (!extension.contains(".")) extension = "." + extension;
this._ext = extension;
this._base = this._name + this._ext;
this._fullPath = Path.joinStringPaths(this._dir, this._base);
this.reparse(this._fullPath);
return this;
}
replaceExtension(searchExt: string, replaceExt: string): Path
{
if (!searchExt.contains(".")) searchExt = "." + searchExt;
if (!replaceExt.contains(".")) replaceExt = "." + replaceExt;
this._ext = this._ext.replace(searchExt, replaceExt);
this._base = this._name + this._ext;
this._fullPath = Path.joinStringPaths(this._dir, this._base);
this.reparse(this._fullPath);
return this;
}
// overide the default toString() method
toString(): string
{
return this.asString;
}
/**
* The root of the path
* @example
* "C:/" or "/".
*/
get root(): string
{
return this._root;
}
/**
* The parent directory of the file, or if the path is a directory this will be the full path.
* @example
* "C:/Users/JohnDoe/Documents" or "/home/johndoe/Documents".
*/
get directory(): Path
{
return new Path(this._dir, this._workingDirectory);
}
/**
* Same as dir, but if the path is a directory this will be the parent directory not the full path.
*/
get parent(): Path
{
return new Path(this._parent, this._workingDirectory);
}
/**
* The name of the file or folder including the extension.
* @example
* "file.txt" or "Documents".
*/
get fullName(): string
{
return this._base;
}
/**
* The extension of the file or folder.
* @example
* ".txt" or "".
*/
get extension(): string
{
return this._ext;
}
get extensionName(): string
{
return this._ext.replace(".", "");
}
/**
* The name of the file or folder without the extension.
* @example
* "file" or "Documents".
*/
get basename(): string
{
return this._name;
}
/**
* The depth of the path.
* @example
* "C:/Users/JohnDoe/Documents/file.txt" = 4
* "/home/johndoe/Documents/file.txt" = 4
* "JohnDoe/Documents/Documents" = 2
*/
get depth(): number
{
return this.asString.replaceAll("\\", "/").replaceAll("//", "/").split("/").length - 1;
}
/**
* The original unparsed uncleaned string that was used to create this path.
* @example
* Can be any string: "C:/Users//John Doe/../Documents\file.txt " or ""
*/
get rawString(): string
{
return this._rawString;
}
/**
* The full path of the file or folder.
* @example
* "C:/Users/John Doe/Documents/file.txt"
* "/home/john doe/Documents/file.txt"
* "C:/Users/John Doe/Documents/Documents"
* "/home/john doe/Documents/Documents"
* "relative/path/to/example.txt"
* "relative/path/to/folder"
*/
get asString(): string
{
return this._fullPath;
}
/**
* True if this is a directory.
*/
get isDirectory(): boolean
{
return this._isDirectory;
}
/**
* True if this is an empty path: ".".
* AKA is the path just referencing its working directory.
*/
get isEmpty(): boolean
{
return this.asString == ".";
}
/**
* True if this is a file, not a folder.
*/
get isFile(): boolean
{
return this._isFile;
}
get workingDirectory(): string
{
return this._workingDirectory;
}
/**
* True if the file or folder exists on the filesystem.
*/
get exists(): boolean
{
if(this._exists == undefined)
{
try
{
this._exists = Path.pathExists(this.absolute().asString);
}
catch (error)
{
this._exists = false;
Path.log("Error checking if path exists: " + this.asString, error, "error");
}
}
return this._exists;
}
get stat(): Stats|undefined
{
if(!this.exists) return;
try
{
let stat = statSync(this.absolute().asString);
return stat;
}
catch (error)
{
Path.log("Error getting stat: " + this.asString, error, "error");
return;
}
}
assertExists(): boolean
{
if(!this.exists)
{
new Notice("Error: Path does not exist: \n\n" + this.asString, 5000);
ExportLog.error("Path does not exist: " + this.asString);
}
return this.exists;
}
get isAbsolute(): boolean
{
let asString = this.asString;
if (asString.startsWith("http:") || asString.startsWith("https:")) return true;
if(this._isWindows)
{
if (asString.match(/^[A-Za-z]:[\\|\/|\\\\|\/\/]/)) return true;
if (asString.startsWith("\\") && !asString.contains(":")) return true;
else return false;
}
else
{
if (asString.startsWith("/")) return true;
else return false;
}
}
get isRelative(): boolean
{
return !this.isAbsolute;
}
get copy(): Path
{
return new Path(this.asString, this._workingDirectory);
}
getDepth(): number
{
return this.asString.split("/").length - 1;
}
absolute(workingDirectory: string | Path = this._workingDirectory): Path
{
return this.copy.makeAbsolute(workingDirectory);
}
validate(options: {allowEmpty?: boolean, requireExists?: boolean, allowAbsolute?: boolean, allowRelative?: boolean, allowTildeHomeDirectory?: boolean, allowFiles?: boolean, allowDirectories?: boolean, requireExtentions?: string[]}): {valid: boolean, isEmpty: boolean, error: string}
{
let error = "";
let valid = true;
let isEmpty = this.rawString.trim() == "";
// remove dots from requireExtention
options.requireExtentions = options.requireExtentions?.map(e => e.replace(".", "")) ?? [];
let dottedExtention = options.requireExtentions.map(e => "." + e);
if (!options.allowEmpty && isEmpty)
{
error += "Path cannot be empty\n";
valid = false;
}
else if (options.allowEmpty && isEmpty)
{
return { valid: true, isEmpty: isEmpty, error: "" };
}
if (options.requireExists && !this.exists)
{
error += "Path does not exist";
valid = false;
}
else if (!options.allowTildeHomeDirectory && this.asString.startsWith("~"))
{
error += "Home directory with tilde (~) is not allowed";
valid = false;
}
else if (!options.allowAbsolute && this.isAbsolute)
{
error += "Path cannot be absolute";
valid = false;
}
else if (!options.allowRelative && this.isRelative)
{
error += "Path cannot be relative";
valid = false;
}
else if (!options.allowFiles && this.isFile)
{
error += "Path cannot be a file";
valid = false;
}
else if (!options.allowDirectories && this.isDirectory)
{
error += "Path cannot be a directory";
valid = false;
}
else if (options.requireExtentions.length > 0 && !options.requireExtentions.includes(this.extensionName) && !isEmpty)
{
error += "Path must be: " + dottedExtention.join(", ");
valid = false;
}
return { valid: valid, isEmpty: isEmpty, error: error };
}
async createDirectory(): Promise<boolean>
{
if (!this.exists)
{
let path = this.absolute().directory.asString;
try
{
await fs.mkdir(path, { recursive: true });
}
catch (error)
{
Path.log("Error creating directory: " + path, error, "error");
return false;
}
}
return true;
}
async readFileString(encoding: "ascii" | "utf8" | "utf-8" | "utf16le" | "ucs2" | "ucs-2" | "base64" | "base64url" | "latin1" | "binary" | "hex" = "utf-8"): Promise<string|undefined>
{
if(!this.exists || this.isDirectory) return;
try
{
let data = await fs.readFile(this.absolute().asString, { encoding: encoding });
return data;
}
catch (error)
{
Path.log("Error reading file: " + this.asString, error, "error");
return;
}
}
async readFileBuffer(): Promise<Buffer|undefined>
{
if(!this.exists || this.isDirectory) return;
try
{
let data = await fs.readFile(this.absolute().asString);
return data;
}
catch (error)
{
Path.log("Error reading file buffer: " + this.asString, error, "error");
return;
}
}
async writeFile(data: string | NodeJS.ArrayBufferView | Iterable<string | NodeJS.ArrayBufferView> | AsyncIterable<string | NodeJS.ArrayBufferView> | internal.Stream, encoding: "ascii" | "utf8" | "utf-8" | "utf16le" | "ucs2" | "ucs-2" | "base64" | "base64url" | "latin1" | "binary" | "hex" = "utf-8"): Promise<boolean>
{
if (this.isDirectory) return false;
try
{
await fs.writeFile(this.absolute().asString, data, { encoding: encoding });
return true;
}
catch (error)
{
let dirExists = await this.createDirectory();
if (!dirExists) return false;
try
{
await fs.writeFile(this.absolute().asString, data, { encoding: encoding });
return true;
}
catch (error)
{
Path.log("Error writing file: " + this.asString, error, "error");
return false;
}
}
}
async delete(recursive: boolean = false): Promise<boolean>
{
if (!this.exists) return false;
try
{
await fs.rm(this.absolute().asString, { recursive: recursive });
return true;
}
catch (error)
{
Path.log("Error deleting file: " + this.asString, error, "error");
return false;
}
}
public static fromString(path: string): Path
{
return new Path(path);
}
private static parsePath(path: string): { root: string, dir: string, parent: string, base: string, ext: string, name: string, fullPath: string }
{
let args = path.split("?")[1] ?? "";
path = path.split("?")[0];
if (process.platform === "win32")
{
if (path.startsWith("~"))
{
path = path.replace("~", homedir());
}
}
try
{
path = decodeURI(path);
}
catch (trash)
{
try
{
path = decodeURI(path.replaceAll("%", ""));
}
catch (e)
{
this.log("Could not decode path:" + path, e, "info");
}
}
let parsed = pathTools.parse(path) as { root: string, dir: string, base: string, ext: string, name: string };
if (parsed.ext.contains(" "))
{
parsed.ext = "";
}
if(parsed.name.endsWith(" "))
{
parsed.name += parsed.ext;
parsed.ext = "";
}
let parent = parsed.dir;
let fullPath = "";
if(path.endsWith("/") || path.endsWith("\\") || parsed.ext == "")
{
if (path.endsWith("/") || path.endsWith("\\")) path = path.substring(0, path.length - 1);
parsed.dir = pathTools.normalizeSafe(path);
let items = parsed.dir.split("/");
parsed.name = items[items.length - 1];
parsed.base = parsed.name;
parsed.ext = "";
fullPath = parsed.dir;
}
else
{
fullPath = pathTools.join(parent, parsed.base);
}
if (args && args.trim() != "") fullPath += "?" + args;
if(fullPath.startsWith("http:")) parsed.root = "http://";
else if(fullPath.startsWith("https:")) parsed.root = "https://";
// make sure that protocols and windows drives use two slashes
parsed.dir = parsed.dir.replace(/[:][\\/](?![\\/])/g, "://");
parent = parsed.dir;
fullPath = fullPath.replace(/[:][\\/](?![\\/])/g, "://");
return { root: parsed.root, dir: parsed.dir, parent: parent, base: parsed.base, ext: parsed.ext, name: parsed.name, fullPath: fullPath };
}
private static pathExists(path: string): boolean
{
return existsSync(path);
}
private static joinStringPaths(...paths: string[]): string
{
let joined = pathTools.join(...paths);
if (joined.startsWith("http"))
{
joined = joined.replaceAll(":/", "://");
}
try
{
return decodeURI(joined);
}
catch (e)
{
this.log("Could not decode joined paths: " + joined, e, "info");
return joined;
}
}
public static joinPath(...paths: Path[]): Path
{
return new Path(Path.joinStringPaths(...paths.map(p => p.asString)), paths[0]._workingDirectory);
}
public static joinStrings(...paths: string[]): Path
{
return new Path(Path.joinStringPaths(...paths));
}
/**
* @param from The source path / working directory
* @param to The destination path
* @returns The relative path to the destination from the source
*/
public static getRelativePath(from: Path, to: Path, useAbsolute: boolean = false): Path
{
let fromUse = useAbsolute ? from.absolute() : from;
let toUse = useAbsolute ? to.absolute() : to;
let relative = pathTools.relative(fromUse.directory.asString, toUse.asString);
let workingDir = from.absolute().directory.asString;
return new Path(relative, workingDir);
}
public static getRelativePathFromVault(path: Path, useAbsolute: boolean = false): Path
{
return Path.getRelativePath(Path.vaultPath, path, useAbsolute);
}
private static vaultPathCache: Path | undefined = undefined;
static get vaultPath(): Path
{
if (this.vaultPathCache != undefined) return this.vaultPathCache;
let adapter = app.vault.adapter;
if (adapter instanceof FileSystemAdapter)
{
let basePath = adapter.getBasePath() ?? "";
this.vaultPathCache = new Path(basePath, "");
return this.vaultPathCache;
}
throw new Error("Vault path could not be determined");
}
private static vaultConfigDirCache: Path | undefined = undefined;
static get vaultConfigDir(): Path
{
if (this.vaultConfigDirCache == undefined)
{
this.vaultConfigDirCache = new Path(app.vault.configDir, "");
}
return this.vaultConfigDirCache;
}
static get emptyPath(): Path
{
return new Path("", "");
}
static get rootPath(): Path
{
return new Path("/", "");
}
static toWebStyle(path: string): string
{
return path.replaceAll(" ", "-").replaceAll(/-{2,}/g, "-").toLowerCase();
}
static equal(path1: string, path2: string): boolean
{
let path1Parsed = new Path(path1).makeUnixStyle().makeWebStyle().asString;
let path2Parsed = new Path(path2).makeUnixStyle().makeWebStyle().asString;
return path1Parsed == path2Parsed;
}
public static async getAllEmptyFoldersRecursive(folder: Path): Promise<Path[]>
{
if (!folder.isDirectory) throw new Error("folder must be a directory: " + folder.asString);
let folders: Path[] = [];
let folderFiles = await fs.readdir(folder.asString);
for (let i = 0; i < folderFiles.length; i++)
{
let file = folderFiles[i];
let path = folder.joinString(file);
if ((await fs.stat(path.asString)).isDirectory())
{
let subFolders = await this.getAllEmptyFoldersRecursive(path);
if (subFolders.length == 0)
{
let subFiles = await fs.readdir(path.asString);
if (subFiles.length == 0) folders.push(path);
}
else
{
folders.push(...subFolders);
}
}
}
return folders;
}
public static async getAllFilesInFolderRecursive(folder: Path): Promise<Path[]>
{
if (!folder.isDirectory) throw new Error("folder must be a directory: " + folder.asString);
let files: Path[] = [];
let folderFiles = await fs.readdir(folder.asString);
for (let i = 0; i < folderFiles.length; i++)
{
let file = folderFiles[i];
let path = folder.joinString(file);
ExportLog.progress(i, folderFiles.length, "Finding Old Files", "Searching: " + folder.asString, "var(--color-yellow)");
if ((await fs.stat(path.asString)).isDirectory())
{
files.push(...await this.getAllFilesInFolderRecursive(path));
}
else
{
files.push(path);
}
}
return files;
}
}

View File

@ -1,37 +0,0 @@
import { PaneType, SplitDirection, TFile, View, WorkspaceLeaf } from "obsidian";
import { ExportLog } from "scripts/html-generation/render-log";
export namespace TabManager
{
function getLeaf(navType: PaneType | boolean, splitDirection: SplitDirection = 'vertical'): WorkspaceLeaf
{
let leaf = navType === 'split' ? app.workspace.getLeaf(navType, splitDirection) : app.workspace.getLeaf(navType);
return leaf;
}
export async function openFileInNewTab(file: TFile, navType: PaneType | boolean, splitDirection: SplitDirection = 'vertical'): Promise<WorkspaceLeaf>
{
let leaf = getLeaf(navType, splitDirection);
try
{
await leaf.openFile(file, undefined).catch((reason) =>
{
ExportLog.error(reason);
});
}
catch (error)
{
ExportLog.error(error);
}
return leaf;
}
export function openNewTab(navType: PaneType | boolean, splitDirection: SplitDirection = 'vertical'): WorkspaceLeaf
{
return getLeaf(navType, splitDirection);
}
}

View File

@ -1,292 +0,0 @@
import { MarkdownView, PluginManifest, TextFileView } from 'obsidian';
import { Path } from './path';
import { ExportLog } from '../html-generation/render-log';
import { Downloadable } from './downloadable';
import { Settings, SettingsPage } from 'scripts/settings/settings';
/* @ts-ignore */
const dialog: Electron.Dialog = require('electron').remote.dialog;
export class Utils
{
static async delay (ms: number)
{
return new Promise( resolve => setTimeout(resolve, ms) );
}
static padStringBeggining(str: string, length: number, char: string)
{
return char.repeat(length - str.length) + str;
}
static includesAny(str: string, substrings: string[]): boolean
{
for (let substring of substrings)
{
if (str.includes(substring)) return true;
}
return false;
}
static async urlAvailable(url: RequestInfo | URL)
{
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), 4000);
const response = await fetch(url, {signal: controller.signal, mode: "no-cors"});
clearTimeout(id);
return response;
}
static sampleCSSColorHex(variable: string, testParentEl: HTMLElement): { a: number, hex: string }
{
let testEl = document.createElement('div');
testEl.style.setProperty('display', 'none');
testEl.style.setProperty('color', 'var(' + variable + ')');
testParentEl.appendChild(testEl);
let col = getComputedStyle(testEl).color;
let opacity = getComputedStyle(testEl).opacity;
testEl.remove();
function toColorObject(str: string)
{
var match = str.match(/rgb?\((\d+),\s*(\d+),\s*(\d+)\)/);
return match ? {
red: parseInt(match[1]),
green: parseInt(match[2]),
blue: parseInt(match[3]),
alpha: 1
} : null
}
var color = toColorObject(col), alpha = parseFloat(opacity);
return isNaN(alpha) && (alpha = 1),
color ? {
a: alpha * color.alpha,
hex: Utils.padStringBeggining(color.red.toString(16), 2, "0") + Utils.padStringBeggining(color.green.toString(16), 2, "0") + Utils.padStringBeggining(color.blue.toString(16), 2, "0")
} : {
a: alpha,
hex: "ffffff"
}
};
static async changeViewMode(view: MarkdownView, modeName: "preview" | "source")
{
/*@ts-ignore*/
const mode = view.modes[modeName];
/*@ts-ignore*/
mode && await view.setMode(mode);
};
static async showSaveDialog(defaultPath: Path, defaultFileName: string, showAllFilesOption: boolean = true): Promise<Path | undefined>
{
// get paths
let absoluteDefaultPath = defaultPath.directory.absolute().joinString(defaultFileName);
// add filters
let filters = [{
name: Utils.trimStart(absoluteDefaultPath.extension, ".").toUpperCase() + " Files",
extensions: [Utils.trimStart(absoluteDefaultPath.extension, ".")]
}];
if (showAllFilesOption)
{
filters.push({
name: "All Files",
extensions: ["*"]
});
}
// show picker
let picker = await dialog.showSaveDialog({
defaultPath: absoluteDefaultPath.asString,
filters: filters,
properties: ["showOverwriteConfirmation"]
})
if (picker.canceled || !picker.filePath) return;
let pickedPath = new Path(picker.filePath);
Settings.exportPath = pickedPath.asString;
SettingsPage.saveSettings();
return pickedPath;
}
static async showSelectFolderDialog(defaultPath: Path): Promise<Path | undefined>
{
if(!defaultPath.exists) defaultPath = Path.vaultPath;
// show picker
let picker = await dialog.showOpenDialog({
defaultPath: defaultPath.directory.asString,
properties: ["openDirectory"]
});
if (picker.canceled) return;
let path = new Path(picker.filePaths[0]);
Settings.exportPath = path.directory.asString;
SettingsPage.saveSettings();
return path;
}
static async showSelectFileDialog(defaultPath: Path): Promise<Path | undefined>
{
if(!defaultPath.exists) defaultPath = Path.vaultPath;
// show picker
let picker = await dialog.showOpenDialog({
defaultPath: defaultPath.directory.asString,
properties: ["openFile"]
});
if (picker.canceled) return;
let path = new Path(picker.filePaths[0]);
return path;
}
static idealDefaultPath() : Path
{
let lastPath = new Path(Settings.exportPath);
if (lastPath.asString != "" && lastPath.exists)
{
return lastPath.directory;
}
return Path.vaultPath;
}
static async downloadFiles(files: Downloadable[], rootPath: Path)
{
if (!rootPath.isAbsolute) throw new Error("folderPath must be absolute: " + rootPath.asString);
ExportLog.progress(0, files.length, "Saving HTML files to disk", "...", "var(--color-green)");
for (let i = 0; i < files.length; i++)
{
let file = files[i];
try
{
await file.download(rootPath.directory);
ExportLog.progress(i+1, files.length, "Saving HTML files to disk", "Saving: " + file.filename, "var(--color-green)");
}
catch (e)
{
ExportLog.error(e.stack, "Could not save file: " + file.filename);
continue;
}
}
}
//async function that awaits until a condition is met
static async waitUntil(condition: () => boolean, timeout: number = 1000, interval: number = 100): Promise<boolean>
{
if (condition()) return true;
return new Promise((resolve, reject) => {
let timer = 0;
let intervalId = setInterval(() => {
if (condition()) {
clearInterval(intervalId);
resolve(true);
} else {
timer += interval;
if (timer >= timeout) {
clearInterval(intervalId);
resolve(false);
}
}
}, interval);
});
}
static getPluginIDs(): string[]
{
/*@ts-ignore*/
let pluginsArray: string[] = Array.from(app.plugins.enabledPlugins.values()) as string[];
for (let i = 0; i < pluginsArray.length; i++)
{
/*@ts-ignore*/
if (app.plugins.manifests[pluginsArray[i]] == undefined)
{
pluginsArray.splice(i, 1);
i--;
}
}
return pluginsArray;
}
static getPluginManifest(pluginID: string): PluginManifest | null
{
// @ts-ignore
return app.plugins.manifests[pluginID] ?? null;
}
static getActiveTextView(): TextFileView | null
{
let view = app.workspace.getActiveViewOfType(TextFileView);
if (!view)
{
return null;
}
return view;
}
static trimEnd(inputString: string, trimString: string): string
{
if (inputString.endsWith(trimString))
{
return inputString.substring(0, inputString.length - trimString.length);
}
return inputString;
}
static trimStart(inputString: string, trimString: string): string
{
if (inputString.startsWith(trimString))
{
return inputString.substring(trimString.length);
}
return inputString;
}
static async openPath(path: Path)
{
// @ts-ignore
await window.electron.remote.shell.openPath(path.asString);
}
static levenshteinDistance(string1: string, string2: string): number
{
if (!string1.length) return string2.length;
if (!string2.length) return string1.length;
const arr = [];
for (let i = 0; i <= string2.length; i++) {
arr[i] = [i];
for (let j = 1; j <= string1.length; j++) {
arr[i][j] =
i === 0
? j
: Math.min(
arr[i - 1][j] + 1,
arr[i][j - 1] + 1,
arr[i - 1][j - 1] + (string1[j - 1] === string2[i - 1] ? 0 : 1)
);
}
}
return arr[string2.length][string1.length];
};
}

View File

@ -1,432 +0,0 @@
/* THIS FILE IS NOT EXPORTED WITH THE HTML FILE! */
/* Flow list used on the settings page */
.flow-list {
contain: inline-size;
gap: 0.2em;
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: flex-start;
width: -webkit-fill-available;
background-color: var(--background-secondary);
border: 1px solid var(--divider-color);
border-radius: 5px;
padding: 6px;
}
.flow-item {
display: flex;
flex-direction: row;
border-radius: 100px;
border: 1px solid var(--divider-color);
font-size: 0.9em;
height: min-content;
width: max-content;
padding: 3px 8px 3px 8px;
margin: 0.1em 0em 0.1em 0.0em;
background-color: var(--background-primary);
align-items: center;
}
.flow-item:has(input:checked) {
background-color: hsla(var(--color-accent-hsl), 0.3);
}
.flow-item input[type="checkbox"] {
padding: 0;
margin: 0.1em;
margin-right: 0.5em;
}
/* Progressbar used in the render progress */
.html-render-progressbar::-webkit-progress-bar {
background-color: var(--background-secondary);
border-radius: 500px;
}
.html-render-progressbar::-webkit-progress-value {
background-color: currentColor;
border-radius: 500px;
}
/*#region Tree */
.tree-container
{
--checkbox-size: 1.2em;
--collapse-arrow-size: 0.5em;
--tree-horizontal-spacing: calc(var(--collapse-arrow-size) * 2);
--tree-vertical-spacing: 0.5em;
--sidebar-margin: 12px;
font-size: 14px;
font-family: var(--font-family);
}
input[type=checkbox].file-checkbox
{
position: absolute;
margin-left: calc(0px - var(--collapse-arrow-size) * 2 - 0.5em - var(--checkbox-size) - 0.5em);
z-index: 20;
}
.theme-dark .tree-item:has(.file-checkbox.checked).mod-tree-folder
{
transition: border-radius 0.2s, background-color 0.2s;
background-color: rgba(var(--color-blue-rgb), 0.05);
border-radius: 3px var(--radius-l) var(--radius-l) 3px;
}
.tree-item:has(.tree-item-contents)
{
cursor: pointer;
}
.tree-item:has(.file-checkbox).mod-tree-folder
{
margin-top: 2px;
margin-bottom: 2px;
}
.tree-item.mod-tree-control
{
background-color: var(--color-base-00);
border-radius: var(--radius-s);
box-shadow: 0px 0px 10px 0px rgba(0,0,0,0.2);
width: fit-content;
margin-bottom: 1em;
}
.tree-item:has(.file-checkbox.checked).mod-tree-folder.is-collapsed
{
border-radius: 3px;
}
.tree-item-title *
{
padding: 0;
margin: 0;
overflow: visible;
display: inline-block;
}
.tree-container .tree-item-icon *
{
color: var(--text-muted);
font-family: emoji;
}
.tree-container .tree-item-icon :is(svg,img)
{
-webkit-mask-image-repeat: no-repeat;
-webkit-mask-image-position: center;
max-width: 1.3em;
height: 100%;
}
/* Skip outer wrappers around icons */
.tree-container .tree-item-icon *:has(svg)
{
display: contents !important;
}
.tree-container .tree-item-icon
{
min-width: 1.6em;
max-width: 1.6em;
display: flex;
align-items: center;
justify-content: flex-start;
}
.theme-dark .tree-item:has(> .tree-link > .tree-item-contents > .file-checkbox:not(.checked)):has(.file-checkbox.checked).mod-tree-folder
{
background-color: rgba(var(--color-pink-rgb), 0.1);
}
.theme-light .tree-item:has(.file-checkbox.checked).mod-tree-folder
{
transition: border-radius 0.2s, background-color 0.2s;
background-color: rgba(var(--color-blue-rgb), 0.15);
border-radius: 3px var(--radius-l) var(--radius-l) 3px;
}
.theme-light .tree-item:has(> .tree-link > .tree-item-contents > .file-checkbox:not(.checked)):has(.file-checkbox.checked).mod-tree-folder
{
background-color: rgba(var(--color-pink-rgb), 0.15);
}
/* Base tree */
.tree-container
{
/* padding-bottom: 12px; */
/* margin: 12px; */
/* height: 100%; */
/* position: relative; */
/* display: contents; */
position: relative;
height: 100%;
width: auto;
margin: var(--sidebar-margin);
margin-top: 3em;
margin-bottom: 0;
}
.tree-container .tree-header
{
display: flex;
flex-direction: row;
align-items: center;
position: absolute;
top: -3em;
}
.tree-container .tree-header .sidebar-section-header
{
margin: 1em;
margin-left: 0;
}
.tree-container:has(.tree-scroll-area:empty)
{
display: none;
}
.tree-container .tree-scroll-area
{
width: 100%;
height: 100%;
max-height: 100%;
overflow-y: auto;
padding: 1em;
padding-right: calc(1em + var(--sidebar-margin));
padding-bottom: 3em;
border-radius: var(--radius-m);
}
.tree-container .tree-item
{
transition: background-color 0.2s;
display: flex;
flex-direction: column;
align-items: flex-start;
padding: 0;
border: none !important;
}
.tree-container .tree-item-children
{
padding: 0;
margin-bottom: 0;
margin-left: 0;
border-left: none;
width: -webkit-fill-available;
}
.tree-container .tree-item.mod-active > .tree-link > .tree-item-contents
{
color: var(--interactive-accent);
}
.tree-container .tree-link {
position: relative;
display: flex;
flex-direction: row;
align-items: center;
text-decoration: none;
color: var(--nav-item-color);
width: -webkit-fill-available;
margin-left: var(--tree-horizontal-spacing);
}
.tree-container .tree-link:active
{
color: var(--nav-item-color-active);
}
.tree-container .tree-item-contents
{
width: 100%;
height: 100%;
margin: 0 !important;
padding: 0 !important;
background-color: transparent !important;
border-radius: var(--radius-s);
padding-left: calc(var(--tree-horizontal-spacing) + var(--checkbox-size) * 2 + 1px) !important;
padding-bottom: calc(var(--tree-vertical-spacing) / 2) !important;
padding-top: calc(var(--tree-vertical-spacing) / 2) !important;
color: var(--text-normal);
display: flex !important;
flex-direction: row !important;
justify-content: flex-start;
align-items: center;
}
.tree-container .tree-item-contents:has(.tree-item-icon.collapse-icon)
{
cursor: pointer !important;
}
.tree-container .tree-item-title
{
overflow: hidden;
text-overflow: ellipsis;
text-wrap: nowrap;
white-space: nowrap;
width: 100%;
width: -webkit-fill-available;
width: -moz-available;
width: fill-available;
position: relative;
}
.tree-container .collapse-icon {
margin-left: calc(0px - var(--collapse-arrow-size) * 2 - 0.5em);
position: absolute;
}
.tree-container .tree-item.mod-tree-folder > .tree-link > .collapse-icon
{
width: 100%;
}
.tree-container .collapse-icon > svg {
color: unset !important;
}
.tree-container .collapse-icon:hover
{
color: var(--nav-item-color-hover);
}
.tree-container .tree-item.is-collapsed > .tree-link > .tree-item-contents > .collapse-icon > svg
{
transition: transform 0.1s ease-in-out;
transform: rotate(-90deg);
}
.tree-container .tree-item-contents:hover
{
cursor: default;
text-decoration: none;
}
.tree-container .tree-link:hover
{
background-color: var(--nav-item-background-hover);
border-radius: var(--radius-s);
}
.tree-container .tree-item-title
{
background-color: transparent !important;
color: var(--nav-item-color) !important;
}
/* Indentation guide */
.tree-container > .tree-scroll-area > * .tree-item
{
margin-left: calc(var(--tree-horizontal-spacing) + var(--collapse-arrow-size) / 2 + 1px);
border-left: var(--nav-indentation-guide-width) solid var(--nav-indentation-guide-color);
}
.tree-container .tree-scroll-area > * > * > .tree-item
{
margin-left: calc(var(--collapse-arrow-size) / 2 + 1px);
}
.tree-container .tree-item.mod-active
{
border-left: var(--nav-indentation-guide-width) solid var(--interactive-accent);
}
.tree-container .tree-item:hover:not(.mod-active):not(.mod-collapsible):not(:has(.tree-item:hover)) /* Hover */
{
border-left: var(--nav-indentation-guide-width) solid var(--nav-item-color-hover);
}
.tree-container .tree-item:not(.mod-collapsible) > .tree-item-children > .tree-item,
.tree-container > .tree-scroll-area > .tree-item,
.tree-container:not(.mod-nav-indicator) .tree-item
{
border-left: none !important;
}
.tree-container .tree-item:not(.mod-collapsible) > .tree-item-children > .tree-item > .tree-link,
.tree-container:not(.mod-nav-indicator) .tree-item .tree-link,
.tree-container > .tree-scroll-area > .tree-item > .tree-link
{
margin-left: 0 !important;
}
/* Special */
/* AnuPpuccin rainbow indent support */
.anp-simple-rainbow-color-toggle.anp-simple-rainbow-indentation-toggle .tree-container .tree-item
{
border-color: rgba(var(--rainbow-folder-color), 0.5);
}
.tree-container.outline-tree .tree-item[data-depth='1'] > .tree-link > .tree-item-contents
{
font-weight: 900;
font-size: 1.1em;
margin-left: 0;
padding-left: 1em;
}
.tree-container .nav-folder.mod-root .nav-folder>.nav-folder-children
{
padding: 0 !important;
margin: 0 !important;
border: none !important;
}
.tree-container .nav-file
{
border-radius: 0 !important;
}
.tree-container .nav-folder.mod-root .nav-folder > .nav-folder-children
{
border-radius: var(--radius-s) !important;;
}
.tree-container .nav-file-tag
{
margin-right: 1em;
}
.tree-container .nav-file-title-content, .tree-container .nav-folder-title-content
{
margin-top: unset !important;
margin-bottom: unset !important;
margin-left: unset !important;
margin-right: unset !important;
display: unset !important;
border-radius: unset !important;
cursor: unset !important;
font-size: unset !important;
font-weight: unset !important;
line-height: unset !important;
padding: unset !important;
border: unset !important;
}
.tree-item-contents:has(.tree-item-icon) .tree-item-title::before
{
display: none !important;
}
/*#endregion */

View File

@ -1,25 +0,0 @@
{
"compilerOptions": {
"baseUrl": ".",
"inlineSourceMap": true,
"inlineSources": true,
"module": "ESNext",
"target": "ES6",
"noImplicitAny": true,
"moduleResolution": "node",
"importHelpers": true,
"isolatedModules": true,
"esModuleInterop":true,
"strictNullChecks": true,
"lib": [
"DOM",
"ES5",
"ES6",
"ES7",
"ES2021.String"
]
},
"include": [
"**/*.ts"
]
}

View File

@ -1,14 +0,0 @@
import { readFileSync, writeFileSync } from "fs";
const targetVersion = process.env.npm_package_version;
// read minAppVersion from manifest.json and bump version to target version
let manifest = JSON.parse(readFileSync("manifest.json", "utf8"));
const { minAppVersion } = manifest;
manifest.version = targetVersion;
writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t"));
// update versions.json with target version and minAppVersion from manifest.json
let versions = JSON.parse(readFileSync("versions.json", "utf8"));
versions[targetVersion] = minAppVersion;
writeFileSync("versions.json", JSON.stringify(versions, null, "\t"));

View File

@ -1,4 +0,0 @@
{
"1.5.2": "0.15.0",
"1.6.0": "1.1.9"
}