Skip to content

Commit a17f55e

Browse files
authored
SQL table extension (#80)
1 parent bccafa7 commit a17f55e

File tree

5 files changed

+291
-6
lines changed

5 files changed

+291
-6
lines changed

docs/Usage.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ Usage of xlog:
6060
Site name is the name that appears on the header beside the logo and in the title tag (default "XLOG")
6161
-source string
6262
Directory that will act as a storage (default "/home/emad/code/xlog")
63+
-sql-table.threshold int
64+
If a table rows is more than this threshold it'll allow users to query it with SQL (default 100)
6365
-theme string
6466
bulma theme to use. (light, dark). empty value means system preference is used
6567
-twitter.username string

docs/official extensions.md

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,32 +6,36 @@ Defined under `/extensions` sub package. each extension is a subpackage. **All e
66
| ActiviyPub | Implements webfinger and activityPub actor and exposing pages as activitypub outbox |
77
| Autolink | Shorten a link string so it wouldn't take unnecessary space |
88
| Autolink pages | Convert a page name mentions in the middle of text to a link |
9-
| Custom CSS | Allow to add custom CSS file to the head of the page |
9+
| blocks | Allows the user to define custom blocks that uses YAML block of codes as input |
1010
| Custom Widget | Allow specifying content that is added in <head> tag, before or after the content |
1111
| Date | Detects dates and converts them to link to a page which lists all pages mentions it |
1212
| Disqus | Add Disqus comments after the view page if -disqus flag is passed |
13+
| Editor | Open the current page in your editor. it uses $EDITOR env variable |
14+
| Embed | Adds a shortcode to embed one page in another page |
1315
| File operations | Add a tool item to delete and rename current page |
16+
| Frontmatter | Allow YAML frontmatter. displayed as properties and can override page title |
1417
| Github | Adds "Edit on github" quick action |
18+
| PGP | PGP key ID to decrypt and edit .md.pgp files using gpg. if empty encryption will be off |
1519
| Hashtags | Add support for hashtags #hashtag syntax |
20+
| Heading | Render heading as a link |
21+
| hotreload | Reload current page when modified on disk |
1622
| HTML | Considers HTML files as pages. supports (html, htm, xhtml) |
1723
| Images | Display consecutive images in columns beside each other instead of under each other |
18-
| Embed | Adds a shortcode to embed one page in another page |
1924
| Link preview | Preview tweets, Facebook posts, youtube videos, Giphy links |
2025
| Manifest | adds manifest.json to head tag and output proper JSON value. |
2126
| MathJax | Support MathJax syntax inline using $ and blocks using $$ |
2227
| Mermaid | Support for MermaidJS graphing library |
2328
| Opengraph | Adds Opengraph meta tags for title, type, image |
2429
| Pandoc | Use pandoc to render documents in other formats as pages like Org-mode files |
2530
| Photos | lists images in a directory similar to instagram |
26-
| PGP | PGP key ID to decrypt and edit .md.pgp files using gpg. if empty encryption will be off |
31+
| Recent | Adds an item to footer to list all pages ordered by last modified page file. |
2732
| RSS | Provides RSS feed served under /+/feed.rss and added to the header of pages |
2833
| RTL | Fixes text direction for RTL languages in the view page |
29-
| Recent | Adds an item to footer to list all pages ordered by last modified page file. |
3034
| Search | Full text search |
3135
| Shortcode | adds a way for short codes (one line and block) |
3236
| Sitemap | adds support for sitemap.xml for search engine crawling |
37+
| sql_table | For long tables adds SQL query form |
3338
| Star | Star pages to pin them to footer |
39+
| TOC | Adds table of contents |
3440
| Todo | allow toggle checkboxes while viewing the page without going to edit mode |
3541
| Upload file | Add support for upload files, screenshots, audio and camera recording |
36-
| Versions | Keeps list of pages older versions |
37-
| Editor | Open the current page in your editor. it uses $EDITOR env variable |

extensions/all/all.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import (
3232
_ "github.com/emad-elsaid/xlog/extensions/search"
3333
_ "github.com/emad-elsaid/xlog/extensions/shortcode"
3434
_ "github.com/emad-elsaid/xlog/extensions/sitemap"
35+
_ "github.com/emad-elsaid/xlog/extensions/sql_table"
3536
_ "github.com/emad-elsaid/xlog/extensions/star"
3637
_ "github.com/emad-elsaid/xlog/extensions/toc"
3738
_ "github.com/emad-elsaid/xlog/extensions/todo"

extensions/sql_table/extension.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package sql_table
2+
3+
import (
4+
"embed"
5+
"flag"
6+
"fmt"
7+
"html/template"
8+
9+
"github.com/emad-elsaid/types"
10+
"github.com/emad-elsaid/xlog"
11+
east "github.com/yuin/goldmark/extension/ast"
12+
)
13+
14+
//go:embed js
15+
var js embed.FS
16+
17+
var sqlTableThreshold int
18+
19+
func init() {
20+
flag.IntVar(&sqlTableThreshold, "sql-table.threshold", 100, "If a table rows is more than this threshold it'll allow users to query it with SQL")
21+
xlog.RegisterExtension(Extension{})
22+
}
23+
24+
type Extension struct{}
25+
26+
func (Extension) Name() string {
27+
return "sql_table"
28+
}
29+
30+
func (Extension) Init() {
31+
xlog.RegisterWidget(xlog.WidgetAfterView, 1, script)
32+
}
33+
34+
func script(p xlog.Page) template.HTML {
35+
if p == nil {
36+
return ""
37+
}
38+
39+
_, a := p.AST()
40+
if a == nil {
41+
return ""
42+
}
43+
44+
tables := xlog.FindAllInAST[*east.Table](a)
45+
if len(tables) == 0 {
46+
return ""
47+
}
48+
49+
largeTableFound := types.Slice[*east.Table](tables).Any(func(t *east.Table) bool {
50+
return len(xlog.FindAllInAST[*east.TableRow](t)) >= sqlTableThreshold
51+
})
52+
if !largeTableFound {
53+
return ""
54+
}
55+
56+
o, _ := js.ReadFile("js/sql_table.html")
57+
o = append(o, []byte(fmt.Sprintf("<script>const sqlTableThreshold = %d</script>", sqlTableThreshold))...)
58+
59+
return template.HTML(o)
60+
}
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
<script src="https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.8.0/sql-wasm.js"></script>
2+
3+
<script>
4+
const tableName = 'input';
5+
6+
async function query(tableElement, sqlQuery) {
7+
// First, convert HTML table to array of objects
8+
function tableToJson(table) {
9+
const headers = [...table.querySelectorAll('th')].map(th => th.textContent.trim());
10+
const rows = [...table.querySelectorAll('tr')].slice(1);
11+
12+
return rows.map(row => {
13+
const cells = [...row.querySelectorAll('td')];
14+
return headers.reduce((obj, header, i) => {
15+
obj[header] = cells[i] ? cells[i].innerHTML.trim() : null;
16+
return obj;
17+
}, {});
18+
});
19+
}
20+
21+
// Convert result back to HTML table
22+
function createResultTable(results) {
23+
const table = document.createElement('table');
24+
25+
// Copy class attribute from input table
26+
if (tableElement.hasAttribute('class')) {
27+
table.setAttribute('class', tableElement.getAttribute('class'));
28+
}
29+
30+
// Create header row if we have results
31+
if (results.length > 0) {
32+
const thead = document.createElement('thead');
33+
const headerRow = document.createElement('tr');
34+
35+
Object.keys(results[0]).forEach(key => {
36+
const th = document.createElement('th');
37+
th.innerHTML = key;
38+
headerRow.appendChild(th);
39+
});
40+
41+
thead.appendChild(headerRow);
42+
table.appendChild(thead);
43+
}
44+
45+
// Create data rows
46+
const tbody = document.createElement('tbody');
47+
results.forEach(row => {
48+
const tr = document.createElement('tr');
49+
Object.values(row).forEach(value => {
50+
const td = document.createElement('td');
51+
td.innerHTML = value;
52+
tr.appendChild(td);
53+
});
54+
tbody.appendChild(tr);
55+
});
56+
57+
table.appendChild(tbody);
58+
return table;
59+
}
60+
61+
62+
try {
63+
// Initialize SQL.js
64+
const SQL = await initSqlJs({
65+
locateFile: file => `https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.8.0/${file}`
66+
});
67+
68+
// Create a database
69+
const db = new SQL.Database();
70+
71+
// Convert table to JSON
72+
const data = tableToJson(tableElement);
73+
74+
// Create table and insert data
75+
if (data.length > 0) {
76+
// Generate CREATE TABLE statement
77+
const columns = Object.keys(data[0])
78+
.map(col => `"${col}" TEXT`)
79+
.join(', ');
80+
81+
db.run(`CREATE TABLE ${tableName} (${columns})`);
82+
83+
// Insert data
84+
data.forEach(row => {
85+
const columns = Object.keys(row).map(col => `"${col}"`).join(', ');
86+
const values = Object.values(row).map(val => `'${val}'`).join(', ');
87+
db.run(`INSERT INTO ${tableName} (${columns}) VALUES (${values})`);
88+
});
89+
90+
// Execute query
91+
const results = db.exec(sqlQuery);
92+
93+
// Convert results to array of objects
94+
if (results.length > 0) {
95+
const columns = results[0].columns;
96+
const rows = results[0].values.map(row => {
97+
return columns.reduce((obj, col, i) => {
98+
obj[col] = row[i];
99+
return obj;
100+
}, {});
101+
});
102+
103+
// Convert to HTML table and return
104+
return createResultTable(rows);
105+
}
106+
}
107+
108+
// Return empty table if no results
109+
return createResultTable([]);
110+
111+
} catch (error) {
112+
console.error('Error executing query:', error);
113+
const errorTable = document.createElement('table');
114+
// Copy class attribute from input table
115+
if (tableElement.hasAttribute('class')) {
116+
errorTable.setAttribute('class', tableElement.getAttribute('class'));
117+
}
118+
errorTable.innerHTML = `<tr><td>Error executing query: ${error.message}</td></tr>`;
119+
return errorTable;
120+
}
121+
}
122+
123+
const template = `
124+
<div class="mb-4">
125+
<div class="field">
126+
<div class="control is-expanded">
127+
<textarea class="textarea" rows="5" placeholder="Enter SQL query. the following table name is input..."></textarea>
128+
</div>
129+
<div class="control">
130+
<button class="button mt-2">Run Query</button>
131+
</div>
132+
</div>
133+
<div class="help is-danger mt-2"></div>
134+
</div>
135+
`;
136+
137+
function initializeTableQueries() {
138+
const tables = document.getElementsByTagName('table');
139+
140+
Array.from(tables).forEach((table) => {
141+
if(table.querySelectorAll('tbody tr').length < sqlTableThreshold){
142+
return
143+
}
144+
145+
// Create template element and parse HTML
146+
const templateEl = document.createElement('template');
147+
templateEl.innerHTML = template.trim();
148+
149+
// Clone the template content
150+
const queryInterface = templateEl.content.cloneNode(true);
151+
152+
// Get references to elements
153+
const wrapper = queryInterface.querySelector('.mb-4');
154+
const input = queryInterface.querySelector('.textarea');
155+
const button = queryInterface.querySelector('.button');
156+
const errorContainer = queryInterface.querySelector('.help');
157+
158+
let resultTable = null;
159+
160+
// Add input event listener
161+
input.addEventListener('input', (e) => {
162+
if (e.target.value.trim() === '') {
163+
table.style.display = '';
164+
if (resultTable) {
165+
resultTable.remove();
166+
resultTable = null;
167+
}
168+
errorContainer.textContent = '';
169+
}
170+
});
171+
172+
// Add button click listener
173+
button.addEventListener('click', async () => {
174+
const sqlQuery = input.value.trim();
175+
176+
if (sqlQuery === '') {
177+
errorContainer.textContent = 'Please enter a SQL query';
178+
return;
179+
}
180+
181+
try {
182+
button.classList.add('is-loading');
183+
184+
if (resultTable) {
185+
resultTable.remove();
186+
}
187+
188+
resultTable = await query(table, sqlQuery);
189+
table.style.display = 'none';
190+
wrapper.after(resultTable);
191+
errorContainer.textContent = '';
192+
193+
} catch (error) {
194+
errorContainer.textContent = `Error: ${error.message}`;
195+
console.error('Query error:', error);
196+
197+
table.style.display = '';
198+
if (resultTable) {
199+
resultTable.remove();
200+
resultTable = null;
201+
}
202+
} finally {
203+
button.classList.remove('is-loading');
204+
}
205+
});
206+
207+
// Insert the query interface before the table
208+
table.parentNode.insertBefore(queryInterface, table);
209+
});
210+
}
211+
212+
// Initialize when DOM is ready
213+
if (document.readyState === 'loading') {
214+
document.addEventListener('DOMContentLoaded', initializeTableQueries);
215+
} else {
216+
initializeTableQueries();
217+
}
218+
</script>

0 commit comments

Comments
 (0)