mirror of
https://github.com/h-sphere/sql-seal.git
synced 2026-07-22 05:12:18 +00:00
docs: adding syntax highlighting to the documentation
This commit is contained in:
parent
77a4f7109e
commit
ef19e25684
12 changed files with 456 additions and 21 deletions
|
|
@ -1,4 +1,5 @@
|
|||
import { defineConfig } from 'vitepress'
|
||||
import ohmMarkdownPlugin from '../plugins/ohm-plugin/vitepress-ohm-plugin'
|
||||
|
||||
// https://vitepress.dev/reference/site-config
|
||||
export default defineConfig({
|
||||
|
|
@ -90,5 +91,10 @@ export default defineConfig({
|
|||
message: '',
|
||||
copyright: 'By <a href="https://hypersphere.blog">hypersphere</a>.<br/>Sponsor Me: <a href="https://ko-fi.com/hypersphere">Ko-Fi</a>'
|
||||
}
|
||||
},
|
||||
markdown: {
|
||||
config(md) {
|
||||
md.use(ohmMarkdownPlugin())
|
||||
},
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,22 +1,22 @@
|
|||
<script setup>
|
||||
const stats = [
|
||||
{
|
||||
number: '2K+',
|
||||
number: '3K+',
|
||||
label: 'Downloads',
|
||||
icon: '📥'
|
||||
},
|
||||
{
|
||||
number: '48+',
|
||||
number: '54+',
|
||||
label: 'Releases',
|
||||
icon: '📦'
|
||||
},
|
||||
{
|
||||
number: '64+',
|
||||
number: '88+',
|
||||
label: 'GitHub Stars',
|
||||
icon: '⭐'
|
||||
},
|
||||
{
|
||||
number: '50+',
|
||||
number: '99+',
|
||||
label: 'Discord Members',
|
||||
icon: '🤝'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { h } from 'vue'
|
|||
import type { Theme } from 'vitepress'
|
||||
import DefaultTheme from 'vitepress/theme'
|
||||
import './style.css'
|
||||
import './syntax-highlighting.css'
|
||||
import Stats from './components/Stats.vue'
|
||||
|
||||
export default {
|
||||
|
|
|
|||
|
|
@ -133,3 +133,8 @@
|
|||
--docsearch-primary-color: var(--vp-c-brand-1) !important;
|
||||
}
|
||||
|
||||
.video-wrapper {
|
||||
margin: 1em auto;
|
||||
display: grid;
|
||||
justify-content: center;
|
||||
}
|
||||
132
docs/.vitepress/theme/syntax-highlighting.css
Normal file
132
docs/.vitepress/theme/syntax-highlighting.css
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
.language-sqlseal {
|
||||
|
||||
|
||||
.vp-blockQuery {
|
||||
--color: #297f30;
|
||||
}
|
||||
|
||||
.vp-blockView {
|
||||
--color: #c834dd;
|
||||
}
|
||||
|
||||
.vp-blockFlag {
|
||||
--color: #d08306;
|
||||
font-weight: bold;
|
||||
color: var(--color);
|
||||
}
|
||||
|
||||
.vp-blockTable {
|
||||
--color: #4985af;
|
||||
}
|
||||
|
||||
.vp-identifier {
|
||||
color: #2d7b33;
|
||||
}
|
||||
|
||||
.vp-function {
|
||||
color: #2c0e9b;
|
||||
}
|
||||
|
||||
.vp-parameter {
|
||||
color: #2c0e9b;
|
||||
}
|
||||
|
||||
.vp-comment {
|
||||
color: #7a7a7a;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.vp-keyword {
|
||||
color: var(--color);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.vp-literal {
|
||||
color: rgb(0, 51, 255);
|
||||
}
|
||||
|
||||
.vp-error {
|
||||
color: rgb(235, 47, 47);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.vp-template-keyword {
|
||||
color: #e3ab53;
|
||||
}
|
||||
|
||||
.vp-blockQuery, .vp-blockTable, .vp-blockFlag, .vp-blockView {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
:is(.vp-blockQuery, .vp-blockTable, .vp-blockFlag, .vp-blockView)::before {
|
||||
background-color: var(--color);
|
||||
width: 5px;
|
||||
top: -3px;
|
||||
bottom: -5px;
|
||||
/* left: 0; */
|
||||
margin-left: -24px;
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
|
||||
.language-sqlseal {
|
||||
display: block;
|
||||
position: relative;
|
||||
background: var(--vp-code-block-bg);
|
||||
padding: 24px 0;
|
||||
text-wrap: wrap;
|
||||
clear: both;
|
||||
overflow-y: scroll;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.dark .language-sqlseal {
|
||||
color: #EEE;
|
||||
--vp-code-block-color: #EEE;
|
||||
}
|
||||
|
||||
.dark .vp-template-keyword {
|
||||
color: #dd8e10;
|
||||
}
|
||||
|
||||
.dark .vp-block-error {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.dark .vp-blockQuery {
|
||||
--color: #5eff6a;
|
||||
}
|
||||
|
||||
.dark .vp-comment {
|
||||
color: #898787;
|
||||
}
|
||||
|
||||
.dark .vp-parameter {
|
||||
color: #95ed22;
|
||||
}
|
||||
|
||||
.dark .vp-literal {
|
||||
color: yellow;
|
||||
}
|
||||
|
||||
.dark .vp-blockView {
|
||||
--color: #e945ff;
|
||||
}
|
||||
|
||||
.dark .vp-blockFlag {
|
||||
--color: #e3ab53;
|
||||
}
|
||||
|
||||
.dark .vp-blockTable {
|
||||
--color: #56b5fa;
|
||||
}
|
||||
|
||||
.dark .vp-function {
|
||||
color: #c4b8ef;
|
||||
}
|
||||
|
||||
.dark .vp-identifier {
|
||||
color: #6ff77a;
|
||||
}
|
||||
|
|
@ -29,7 +29,7 @@ hero:
|
|||
<div class="content">
|
||||
Turn your Obsidian vault into a powerful, queryable database using SQL. SQLSeal seamlessly integrates with your notes and CSV and JSON files, enabling you to extract insights and patterns from your personal knowledge base.
|
||||
|
||||
```sql
|
||||
```sqlseal
|
||||
SELECT * FROM files
|
||||
ORDER BY modified_at DESC
|
||||
LIMIT 10
|
||||
|
|
@ -71,7 +71,7 @@ LIMIT 10
|
|||
## Query any data
|
||||
You can query any CSV and JSON data stored in your vault. SQLSeal can help you analyse your finances, workouts, goals and whatever you imagine!
|
||||
|
||||
```sql
|
||||
```sqlseal
|
||||
TABLE commute = file(transport.csv)
|
||||
|
||||
SELECT
|
||||
|
|
@ -97,9 +97,6 @@ GROUP BY Date, Transport_Type
|
|||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="vp-doc">
|
||||
<div class="container-split reverse">
|
||||
|
||||
|
|
@ -122,6 +119,16 @@ SQLSeal Charts is currently being reviewed by Obsidian. As soon as it gets appro
|
|||
</div>
|
||||
|
||||
|
||||
## Learn more
|
||||
<p>This introduction video should help you get started with basics of SQLSeal</p>
|
||||
<div class="video-wrapper">
|
||||
<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/yXDrpWDeQPg?si=rttPk5LdH2lahqRN" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<Stats />
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -5,14 +5,14 @@ SQLSeal allows for rendering links and images. For now the images needs to be ex
|
|||
|
||||
## Links
|
||||
To display a link, wrap use the `a` SQL function, for example:
|
||||
```sql
|
||||
```sqlseal
|
||||
SELECT a(path) FROM files LIMIT 10
|
||||
```
|
||||

|
||||
|
||||
You can use second parameter to provide name for the link:
|
||||
|
||||
```sql
|
||||
```sqlseal
|
||||
SELECT a(path, name) from files LIMIT 10
|
||||
```
|
||||
|
||||
|
|
@ -22,14 +22,14 @@ This API works for both filesystem and CSV files.
|
|||
## Images
|
||||
You can embed images within your results. You need to wrap your resulting column with `img` function.
|
||||
|
||||
```sql
|
||||
```sqlseal
|
||||
SELECT name, img(coverImg) FROM files
|
||||
```
|
||||
|
||||
### Local images
|
||||
When using local images (stored in Obsidian) you need to pass second parameter being path of the original note. For example:
|
||||
|
||||
```sql
|
||||
```sqlseal
|
||||
SELECT path, img(cover, path) FROM files
|
||||
```
|
||||
|
||||
|
|
@ -37,7 +37,7 @@ SELECT path, img(cover, path) FROM files
|
|||
The example below uses [Goodreads-books](https://www.kaggle.com/datasets/jealousleopard/goodreadsbooks) Kaggle dataset in CSV loaded in obsidian to display books with links to Open Library and showing the covers from Open Library Cover API. It uses Obsidian property to filter the author name.
|
||||
|
||||
|
||||
```sql
|
||||
```sqlseal
|
||||
TABLE books = file(books.csv)
|
||||
|
||||
SELECT
|
||||
|
|
@ -56,6 +56,6 @@ LIMIT 10
|
|||
|
||||
## Checkboxes
|
||||
You can display boolean data as checkbox in the interface by calling `checkbox` function:
|
||||
```sql
|
||||
```sqlseal
|
||||
SELECT date, checkbox(excercised) FROM files WHERE date is not null
|
||||
```
|
||||
275
docs/plugins/ohm-plugin/vitepress-ohm-plugin.js
Normal file
275
docs/plugins/ohm-plugin/vitepress-ohm-plugin.js
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
// .vitepress/ohmMarkdownPlugin.js
|
||||
import * as ohm from 'ohm-js'
|
||||
import { SQLSealLangDefinition } from '../../../src/grammar/parser'
|
||||
import { highlighterOperation } from '../../../src/grammar/highlighterOperation'
|
||||
|
||||
const parser = (sql) => {
|
||||
const defaultViews = [
|
||||
{
|
||||
name: 'GRID',
|
||||
singleLine: false,
|
||||
argument: 'anyObject?'
|
||||
},
|
||||
{
|
||||
name: 'HTML',
|
||||
singleLine: true,
|
||||
argument: 'restLine?'
|
||||
},
|
||||
{
|
||||
name: 'MARKDOWN',
|
||||
singleLine: true,
|
||||
argument: 'restLine?'
|
||||
},
|
||||
{
|
||||
name: 'LIST',
|
||||
singleLine: true,
|
||||
argument: 'restLine?'
|
||||
},
|
||||
{
|
||||
name: 'TEMPLATE',
|
||||
singleLine: false,
|
||||
argument: 'handlebarsTemplate'
|
||||
}
|
||||
]
|
||||
|
||||
const grammar = ohm.grammar(SQLSealLangDefinition(defaultViews, [], false))
|
||||
const match = grammar.match(sql)
|
||||
if (match.failed()) {
|
||||
return []
|
||||
}
|
||||
const highlight = highlighterOperation(grammar)(match)
|
||||
|
||||
const results = highlight.highlight()
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a markdown-it plugin that integrates Ohm.js parsing for code blocks
|
||||
*
|
||||
* @returns {Function} A markdown-it plugin function
|
||||
*/
|
||||
export default function ohmMarkdownPlugin() {
|
||||
// Languages we want to parse with Ohm.js
|
||||
const supportedLanguages = ['js', 'javascript', 'ts', 'typescript', 'jsx', 'tsx', 'sqlseal']
|
||||
|
||||
// Track processed tokens to prevent duplicate processing
|
||||
const processedTokens = new WeakSet()
|
||||
|
||||
/**
|
||||
* This is the actual markdown-it plugin function that gets registered
|
||||
* with markdown-it.use()
|
||||
*/
|
||||
return (md) => {
|
||||
// Store the original fence renderer
|
||||
const originalFence = md.renderer.rules.fence
|
||||
|
||||
// Replace it with our custom implementation
|
||||
md.renderer.rules.fence = (tokens, idx, options, env, slf) => {
|
||||
const token = tokens[idx]
|
||||
|
||||
// Check if we've already processed this token to prevent duplicate processing
|
||||
if (processedTokens.has(token)) {
|
||||
return originalFence(tokens, idx, options, env, slf)
|
||||
}
|
||||
|
||||
// Mark this token as processed
|
||||
processedTokens.add(token)
|
||||
|
||||
const code = token.content
|
||||
const lang = token.info.trim()
|
||||
|
||||
// Skip unsupported languages
|
||||
if (!supportedLanguages.includes(lang)) {
|
||||
return originalFence(tokens, idx, options, env, slf)
|
||||
}
|
||||
|
||||
try {
|
||||
// Parse the code with Ohm.js
|
||||
const rawDecorators = parser(code)
|
||||
|
||||
// If no decorators found, use the original renderer
|
||||
if (!rawDecorators || rawDecorators.length === 0) {
|
||||
return originalFence(tokens, idx, options, env, slf)
|
||||
}
|
||||
|
||||
// Process decorators with block types prioritized to create nested structure
|
||||
const html = buildNestedDecorators(code, rawDecorators, lang)
|
||||
|
||||
return html
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error parsing code with Ohm.js:', error)
|
||||
return originalFence(tokens, idx, options, env, slf)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds nested HTML structure where block decorators properly
|
||||
* encapsulate smaller decorators
|
||||
*
|
||||
* @param {string} code - The code to be decorated
|
||||
* @param {Array} decorators - The decorators to apply
|
||||
* @param {string} lang - The language of the code
|
||||
* @returns {string} HTML with properly nested decorators
|
||||
*/
|
||||
function buildNestedDecorators(code, decorators, lang) {
|
||||
if (!decorators || decorators.length === 0) {
|
||||
return `<pre class="language-${lang} vp-custom-code-block"><code class="language-${lang}">${escapeHtml(code)}</code></pre>`
|
||||
}
|
||||
|
||||
// Sort decorators by priority (block types first) and then by size (largest first)
|
||||
const sortedDecorators = [...decorators].sort((a, b) => {
|
||||
// First prioritize block types
|
||||
const aIsBlock = a.type.startsWith('block')
|
||||
const bIsBlock = b.type.startsWith('block')
|
||||
|
||||
if (aIsBlock && !bIsBlock) return -1
|
||||
if (!aIsBlock && bIsBlock) return 1
|
||||
|
||||
// If both are blocks or both are not blocks, sort by size (larger first)
|
||||
const aSize = a.end - a.start
|
||||
const bSize = b.end - b.start
|
||||
|
||||
return bSize - aSize
|
||||
})
|
||||
|
||||
// Create a tree representation of the decorators
|
||||
const decoratorTree = buildDecoratorTree(sortedDecorators)
|
||||
|
||||
// Render the HTML with the tree
|
||||
let html = `<pre class="language-${lang} vp-custom-code-block"><code class="language-${lang}">`
|
||||
html += renderDecoratorTree(decoratorTree, code)
|
||||
html += '</code></pre>'
|
||||
|
||||
return html
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a tree representation of decorators where parent nodes
|
||||
* contain child nodes
|
||||
*
|
||||
* @param {Array} decorators - Sorted decorators array
|
||||
* @returns {Array} Tree structure of decorators
|
||||
*/
|
||||
function buildDecoratorTree(decorators) {
|
||||
// Initialize with the entire code range as the root
|
||||
const root = {
|
||||
start: 0,
|
||||
end: Infinity,
|
||||
children: []
|
||||
}
|
||||
|
||||
// Function to add a decorator to the tree
|
||||
function addToTree(node, decorator) {
|
||||
// Check if this decorator fits within any of the children
|
||||
for (let i = 0; i < node.children.length; i++) {
|
||||
const child = node.children[i]
|
||||
|
||||
// If the decorator fits entirely within this child
|
||||
if (decorator.start >= child.start && decorator.end <= child.end) {
|
||||
return addToTree(child, decorator)
|
||||
}
|
||||
}
|
||||
|
||||
// This decorator doesn't fit within any child, so it's a direct child of this node
|
||||
const newNode = {
|
||||
...decorator,
|
||||
children: []
|
||||
}
|
||||
|
||||
// Any existing children that fit within this new node should become its children
|
||||
for (let i = node.children.length - 1; i >= 0; i--) {
|
||||
const child = node.children[i]
|
||||
|
||||
if (child.start >= decorator.start && child.end <= decorator.end) {
|
||||
newNode.children.push(child)
|
||||
node.children.splice(i, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// Sort the children by start position
|
||||
newNode.children.sort((a, b) => a.start - b.start)
|
||||
|
||||
// Add the new node to the current node's children
|
||||
node.children.push(newNode)
|
||||
|
||||
// Sort the children by start position
|
||||
node.children.sort((a, b) => a.start - b.start)
|
||||
}
|
||||
|
||||
// Add each decorator to the tree
|
||||
for (const decorator of decorators) {
|
||||
addToTree(root, decorator)
|
||||
}
|
||||
|
||||
return root
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the decorator tree as HTML
|
||||
*
|
||||
* @param {Object} node - Current tree node
|
||||
* @param {string} code - The original code
|
||||
* @param {number} lastPos - Last rendered position in the code
|
||||
* @returns {string} HTML representation of the tree
|
||||
*/
|
||||
function renderDecoratorTree(node, code, lastPos = 0) {
|
||||
let html = ''
|
||||
let currentPos = lastPos
|
||||
|
||||
// If this is not the root node, add its content
|
||||
if (node.type) {
|
||||
html += `<span class="vp-code-decorator vp-${node.type}" data-type="${node.type}">`
|
||||
}
|
||||
|
||||
// Add the children with the text between them
|
||||
for (const child of node.children) {
|
||||
// Add text before this child
|
||||
if (child.start > currentPos) {
|
||||
html += escapeHtml(code.substring(currentPos, child.start))
|
||||
}
|
||||
|
||||
// Add the child
|
||||
html += renderDecoratorTree(child, code, child.start)
|
||||
|
||||
// Update the current position
|
||||
currentPos = child.end
|
||||
}
|
||||
|
||||
// Add the remaining text in this node
|
||||
const endPos = node.type ? node.end : code.length
|
||||
if (currentPos < endPos) {
|
||||
html += escapeHtml(code.substring(currentPos, endPos))
|
||||
}
|
||||
|
||||
// Close the tag if this is not the root
|
||||
if (node.type) {
|
||||
html += '</span>'
|
||||
}
|
||||
|
||||
return html
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes HTML and additionally escapes handlebars/mustache syntax
|
||||
* to prevent Vue from interpreting them as Vue template directives
|
||||
*/
|
||||
function escapeHtml(unsafe) {
|
||||
// First do regular HTML escaping
|
||||
let escaped = unsafe
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
|
||||
// Then escape the handlebars/mustache syntax by adding a zero-width space
|
||||
// between the opening braces to prevent Vue from interpreting them
|
||||
escaped = escaped
|
||||
.replace(/{{/g, "{​{")
|
||||
.replace(/}}/g, "}​}");
|
||||
|
||||
return escaped;
|
||||
}
|
||||
|
|
@ -16,7 +16,7 @@ The following variables are exposed
|
|||
|
||||
|
||||
## Example
|
||||
```sql
|
||||
```sqlseal
|
||||
TEMPLATE
|
||||
Current Path: {{properties.path}}
|
||||
{{#each data}}
|
||||
|
|
|
|||
|
|
@ -15,11 +15,11 @@ On top of properties current file exposes in the properties, you can also use th
|
|||
|
||||
### Example 1: Selecting just the current file from the files table
|
||||
|
||||
```sql
|
||||
```sqlseal
|
||||
SELECT * FROM files WHERE path = @path
|
||||
```
|
||||
|
||||
### Example2: Select tags of the current file
|
||||
```sql
|
||||
```sqlseal
|
||||
SELECT * FROM tags WHERE path = @path
|
||||
```
|
||||
|
|
@ -54,7 +54,10 @@ export const highlighterOperation = (grammar: ohm.Grammar) => {
|
|||
|
||||
s.addOperation<any>('highlight', {
|
||||
_terminal() {
|
||||
return computeNodeHighlight(this)
|
||||
return [
|
||||
...computeNodeHighlight(this),
|
||||
...this.children.map(c => c.highlight()).flat()
|
||||
]
|
||||
},
|
||||
_nonterminal() {
|
||||
return [
|
||||
|
|
@ -74,6 +77,13 @@ export const highlighterOperation = (grammar: ohm.Grammar) => {
|
|||
}
|
||||
return [...computeNodeHighlight(this), ...results]
|
||||
},
|
||||
comment(_comment) {
|
||||
return [{
|
||||
type: 'comment',
|
||||
start: this.source.startIdx,
|
||||
end: this.source.endIdx
|
||||
}]
|
||||
},
|
||||
errorLine: (node, _nl) => {
|
||||
return [{
|
||||
type: 'error',
|
||||
|
|
|
|||
|
|
@ -25,6 +25,5 @@
|
|||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts"
|
||||
]
|
||||
"**/*.ts"]
|
||||
}
|
||||
Loading…
Reference in a new issue