Initial implementation
This commit is contained in:
21
src/App.vue
Normal file
21
src/App.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<header class="bg-blue-600 text-white p-4">
|
||||
<h1 class="text-2xl font-bold">OrgTree</h1>
|
||||
<p class="text-blue-100">Visualize and edit your org-mode files</p>
|
||||
</header>
|
||||
<main class="min-h-screen bg-gray-50">
|
||||
<RouterView />
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { RouterView } from 'vue-router'
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#app {
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
}
|
||||
</style>
|
||||
32
src/components/FileManager/FileTabs.vue
Normal file
32
src/components/FileManager/FileTabs.vue
Normal file
@@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<div class="border-b border-gray-200 bg-white">
|
||||
<nav class="flex space-x-1 overflow-x-auto">
|
||||
<button
|
||||
v-for="file in files"
|
||||
:key="file.id"
|
||||
@click="$emit('file-selected', file)"
|
||||
:class="[
|
||||
'px-4 py-2 text-sm font-medium whitespace-nowrap',
|
||||
file.id === activeFile?.id
|
||||
? 'border-b-2 border-blue-500 text-blue-600'
|
||||
: 'text-gray-500 hover:text-gray-700'
|
||||
]"
|
||||
>
|
||||
{{ file.name }}
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { OrgFile } from '@/types/org'
|
||||
|
||||
defineProps<{
|
||||
files: OrgFile[]
|
||||
activeFile: OrgFile | null
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
'file-selected': [file: OrgFile]
|
||||
}>()
|
||||
</script>
|
||||
91
src/components/FileManager/FileUploader.vue
Normal file
91
src/components/FileManager/FileUploader.vue
Normal file
@@ -0,0 +1,91 @@
|
||||
<template>
|
||||
<div class="upload-area">
|
||||
<div
|
||||
class="border-2 border-dashed border-gray-300 rounded-lg p-8 text-center hover:border-blue-400 transition-colors"
|
||||
:class="{ 'border-blue-400 bg-blue-50': isDragOver }"
|
||||
@dragover.prevent="isDragOver = true"
|
||||
@dragleave.prevent="isDragOver = false"
|
||||
@drop.prevent="handleDrop"
|
||||
>
|
||||
<div class="space-y-4">
|
||||
<div class="text-4xl text-gray-400">📁</div>
|
||||
<div class="space-y-3">
|
||||
<p class="text-lg text-gray-600">Drop org files or folders here</p>
|
||||
<p class="text-sm text-gray-400">or</p>
|
||||
<div class="flex gap-3 justify-center">
|
||||
<label class="inline-block">
|
||||
<input
|
||||
type="file"
|
||||
multiple
|
||||
accept=".org"
|
||||
@change="handleFileSelect"
|
||||
class="hidden"
|
||||
/>
|
||||
<span class="bg-blue-600 text-white px-4 py-2 rounded cursor-pointer hover:bg-blue-700">
|
||||
Browse Files
|
||||
</span>
|
||||
</label>
|
||||
<button
|
||||
@click="loadExampleFiles"
|
||||
class="bg-green-600 text-white px-4 py-2 rounded hover:bg-green-700"
|
||||
>
|
||||
Use Example Files
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { parseOrgFile } from '@/utils/orgParser'
|
||||
import { exampleFiles } from '@/data/exampleFiles'
|
||||
import type { OrgFile } from '@/types/org'
|
||||
|
||||
const emit = defineEmits<{
|
||||
'files-selected': [files: OrgFile[]]
|
||||
}>()
|
||||
|
||||
const isDragOver = ref(false)
|
||||
|
||||
async function handleDrop(event: DragEvent) {
|
||||
isDragOver.value = false
|
||||
const files = Array.from(event.dataTransfer?.files || [])
|
||||
await processFiles(files)
|
||||
}
|
||||
|
||||
async function handleFileSelect(event: Event) {
|
||||
const target = event.target as HTMLInputElement
|
||||
const files = Array.from(target.files || [])
|
||||
await processFiles(files)
|
||||
}
|
||||
|
||||
async function processFiles(files: File[]) {
|
||||
const orgFiles: OrgFile[] = []
|
||||
|
||||
for (const file of files) {
|
||||
if (file.name.endsWith('.org')) {
|
||||
const content = await file.text()
|
||||
const parsed = parseOrgFile(file.name, content)
|
||||
orgFiles.push(parsed)
|
||||
}
|
||||
}
|
||||
|
||||
if (orgFiles.length > 0) {
|
||||
emit('files-selected', orgFiles)
|
||||
}
|
||||
}
|
||||
|
||||
function loadExampleFiles() {
|
||||
const orgFiles: OrgFile[] = []
|
||||
|
||||
for (const [filename, content] of Object.entries(exampleFiles)) {
|
||||
const parsed = parseOrgFile(filename, content)
|
||||
orgFiles.push(parsed)
|
||||
}
|
||||
|
||||
emit('files-selected', orgFiles)
|
||||
}
|
||||
</script>
|
||||
112
src/components/OrgTree/OrgNode.vue
Normal file
112
src/components/OrgTree/OrgNode.vue
Normal file
@@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<div class="org-node">
|
||||
<div class="flex items-start space-x-3 p-2 hover:bg-gray-50 rounded">
|
||||
<button
|
||||
v-if="node.children.length > 0"
|
||||
@click="toggleExpanded"
|
||||
class="mt-1 text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
{{ isExpanded ? '▼' : '▶' }}
|
||||
</button>
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center space-x-2 mb-1">
|
||||
<span class="text-gray-400">{{ '*'.repeat(node.level) }}</span>
|
||||
<input
|
||||
v-if="isEditing"
|
||||
v-model="editTitle"
|
||||
@blur="saveTitle"
|
||||
@keyup.enter="saveTitle"
|
||||
class="font-semibold bg-transparent border-b border-gray-300 focus:outline-none focus:border-blue-500"
|
||||
/>
|
||||
<h3
|
||||
v-else
|
||||
@dblclick="startEditing"
|
||||
class="font-semibold cursor-pointer hover:text-blue-600"
|
||||
>
|
||||
{{ node.title }}
|
||||
</h3>
|
||||
<div v-if="node.tags" class="flex space-x-1">
|
||||
<span
|
||||
v-for="tag in node.tags"
|
||||
:key="tag"
|
||||
class="px-2 py-1 bg-blue-100 text-blue-800 text-xs rounded"
|
||||
>
|
||||
{{ tag }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="node.content" class="text-sm text-gray-600 ml-4">
|
||||
<textarea
|
||||
v-if="isEditingContent"
|
||||
v-model="editContent"
|
||||
@blur="saveContent"
|
||||
class="w-full bg-transparent border border-gray-300 rounded p-2 focus:outline-none focus:border-blue-500"
|
||||
rows="3"
|
||||
/>
|
||||
<pre
|
||||
v-else
|
||||
@dblclick="startEditingContent"
|
||||
class="cursor-pointer hover:bg-gray-100 p-1 rounded"
|
||||
>{{ node.content }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="isExpanded && node.children.length > 0" class="ml-6 border-l-2 border-gray-200">
|
||||
<OrgNode
|
||||
v-for="child in node.children"
|
||||
:key="child.id"
|
||||
:node="child"
|
||||
@update="$emit('update', $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import type { OrgNode as IOrgNode } from '@/types/org'
|
||||
|
||||
const props = defineProps<{
|
||||
node: IOrgNode
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
update: [node: IOrgNode]
|
||||
}>()
|
||||
|
||||
const isExpanded = ref(true)
|
||||
const isEditing = ref(false)
|
||||
const isEditingContent = ref(false)
|
||||
const editTitle = ref(props.node.title)
|
||||
const editContent = ref(props.node.content)
|
||||
|
||||
function toggleExpanded() {
|
||||
isExpanded.value = !isExpanded.value
|
||||
}
|
||||
|
||||
function startEditing() {
|
||||
isEditing.value = true
|
||||
editTitle.value = props.node.title
|
||||
}
|
||||
|
||||
function saveTitle() {
|
||||
isEditing.value = false
|
||||
if (editTitle.value !== props.node.title) {
|
||||
emit('update', { ...props.node, title: editTitle.value })
|
||||
}
|
||||
}
|
||||
|
||||
function startEditingContent() {
|
||||
isEditingContent.value = true
|
||||
editContent.value = props.node.content
|
||||
}
|
||||
|
||||
function saveContent() {
|
||||
isEditingContent.value = false
|
||||
if (editContent.value !== props.node.content) {
|
||||
emit('update', { ...props.node, content: editContent.value })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
48
src/components/OrgTree/OrgTreeView.vue
Normal file
48
src/components/OrgTree/OrgTreeView.vue
Normal file
@@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h2 class="text-xl font-semibold">{{ file.name }}</h2>
|
||||
<button
|
||||
@click="exportFile"
|
||||
class="bg-green-600 text-white px-4 py-2 rounded hover:bg-green-700"
|
||||
>
|
||||
Export
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="org-tree">
|
||||
<OrgNode
|
||||
v-for="node in file.nodes"
|
||||
:key="node.id"
|
||||
:node="node"
|
||||
@update="updateNode"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import OrgNode from './OrgNode.vue'
|
||||
import { generateOrgContent } from '@/utils/orgGenerator'
|
||||
import type { OrgFile, OrgNode as IOrgNode } from '@/types/org'
|
||||
|
||||
const props = defineProps<{
|
||||
file: OrgFile
|
||||
}>()
|
||||
|
||||
function updateNode(updatedNode: IOrgNode) {
|
||||
// TODO: Implement node update logic
|
||||
console.log('Node updated:', updatedNode)
|
||||
}
|
||||
|
||||
function exportFile() {
|
||||
const content = generateOrgContent(props.file)
|
||||
const blob = new Blob([content], { type: 'text/plain' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = props.file.name
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
</script>
|
||||
277
src/data/exampleFiles.ts
Normal file
277
src/data/exampleFiles.ts
Normal file
@@ -0,0 +1,277 @@
|
||||
export const exampleFiles = {
|
||||
'orgtree-project.org': `#+TITLE: OrgTree Project
|
||||
#+AUTHOR: Development Team
|
||||
#+DATE: 2024-01-01
|
||||
#+TAGS: project development vue
|
||||
|
||||
* Project Overview
|
||||
OrgTree is a web-based interface for visualizing org-mode files as interactive trees.
|
||||
|
||||
** Goals
|
||||
- [ ] Create intuitive tree visualization
|
||||
- [X] Support file import/export
|
||||
- [ ] Enable real-time editing
|
||||
|
||||
** Technical Stack
|
||||
*** Frontend
|
||||
- Vue.js 3 with TypeScript
|
||||
- Tailwind CSS for styling
|
||||
- Vite for build tooling
|
||||
|
||||
*** Backend (Future)
|
||||
- Node.js with Express
|
||||
- Database for persistence
|
||||
|
||||
* Development Phases
|
||||
** Phase 1: Core Implementation :DONE:
|
||||
Basic functionality is working.
|
||||
|
||||
*** File Parser
|
||||
The parser handles standard org-mode syntax including:
|
||||
- Headings with multiple levels
|
||||
- TODO states
|
||||
- Tags and properties
|
||||
- Content blocks
|
||||
|
||||
** Phase 2: Advanced Features :CURRENT:
|
||||
*** Drag and Drop
|
||||
Enable users to reorganize tree structure visually.
|
||||
|
||||
*** Node Management
|
||||
- Add new nodes
|
||||
- Delete existing nodes
|
||||
- Modify node properties
|
||||
|
||||
** Phase 3: Polish :FUTURE:
|
||||
*** Performance
|
||||
Optimize for large org files with hundreds of nodes.
|
||||
|
||||
*** Accessibility
|
||||
Ensure keyboard navigation and screen reader support.
|
||||
|
||||
* Notes
|
||||
** Implementation Details
|
||||
The current parsing approach uses regex-based extraction which works well for standard org-mode files.
|
||||
|
||||
** Known Issues
|
||||
- Large files may cause performance problems
|
||||
- Some advanced org-mode features not yet supported`,
|
||||
|
||||
'daily-notes.org': `#+TITLE: Daily Notes
|
||||
#+FILETAGS: personal journal
|
||||
|
||||
* 2024-01-15 Monday
|
||||
** Morning Routine :habit:
|
||||
- [X] Exercise for 30 minutes
|
||||
- [X] Read for 20 minutes
|
||||
- [ ] Meditate for 10 minutes
|
||||
|
||||
** Work Tasks
|
||||
*** OrgTree Development
|
||||
**** Code Review
|
||||
Review the parsing logic for edge cases.
|
||||
|
||||
**** Bug Fixes
|
||||
- Fix tag parsing issue
|
||||
- Update export functionality
|
||||
|
||||
*** Meetings
|
||||
**** 10:00 AM - Team Standup
|
||||
Discuss progress on current sprint.
|
||||
|
||||
**** 2:00 PM - Project Review
|
||||
Present OrgTree demo to stakeholders.
|
||||
|
||||
** Evening Reflection
|
||||
Today was productive. The OrgTree project is making good progress.
|
||||
|
||||
*** What went well
|
||||
- Completed parsing implementation
|
||||
- Fixed several UI bugs
|
||||
- Good feedback from team
|
||||
|
||||
*** Areas for improvement
|
||||
- Need to focus more on testing
|
||||
- Documentation needs updates
|
||||
|
||||
* 2024-01-16 Tuesday
|
||||
** Goals for Today
|
||||
- [ ] Implement drag-and-drop functionality
|
||||
- [ ] Create more comprehensive test files
|
||||
- [ ] Update project documentation
|
||||
|
||||
** Learning
|
||||
*** Org Mode Features
|
||||
Discovered some advanced org-mode syntax:
|
||||
- PROPERTIES drawers
|
||||
- Custom TODO keywords
|
||||
- Scheduling and deadlines
|
||||
|
||||
** Random Thoughts
|
||||
The tree visualization really helps see the structure of complex org files. This tool could be useful for many people who work with large documentation sets.`,
|
||||
|
||||
'reading-list.org': `#+TITLE: Reading List
|
||||
#+FILETAGS: books reading personal
|
||||
|
||||
* Currently Reading :READING:
|
||||
** "The Pragmatic Programmer" by Andy Hunt :programming:
|
||||
*** Progress
|
||||
- [X] Chapter 1: A Pragmatic Philosophy
|
||||
- [X] Chapter 2: A Pragmatic Approach
|
||||
- [ ] Chapter 3: The Basic Tools
|
||||
- [ ] Chapter 4: Pragmatic Paranoia
|
||||
|
||||
*** Key Takeaways
|
||||
**** DRY Principle
|
||||
Don't Repeat Yourself - avoid duplication in code and knowledge.
|
||||
|
||||
**** Orthogonality
|
||||
Design systems where components don't depend unnecessarily on each other.
|
||||
|
||||
*** Notes
|
||||
This book emphasizes the importance of craftsmanship in software development.
|
||||
|
||||
* To Read :TODO:
|
||||
** Technical Books
|
||||
*** "Clean Architecture" by Robert Martin :programming:
|
||||
**** Priority
|
||||
High - recommended by multiple colleagues
|
||||
|
||||
**** Notes
|
||||
Focuses on software architecture principles and design patterns.
|
||||
|
||||
*** "Designing Data-Intensive Applications" by Martin Kleppmann :systems:
|
||||
**** Priority
|
||||
Medium - good for understanding distributed systems
|
||||
|
||||
** Fiction
|
||||
*** "The Three-Body Problem" by Liu Cixin :scifi:
|
||||
**** Priority
|
||||
High - won Hugo Award
|
||||
|
||||
**** Description
|
||||
Hard science fiction exploring first contact with alien civilization.
|
||||
|
||||
* Completed :DONE:
|
||||
** "Atomic Habits" by James Clear :productivity:
|
||||
*** Rating
|
||||
5/5 - Excellent practical advice on habit formation
|
||||
|
||||
*** Key Concepts
|
||||
**** 1% Better Every Day
|
||||
Small improvements compound over time.
|
||||
|
||||
**** Habit Stacking
|
||||
Link new habits to existing routines.
|
||||
|
||||
**** Environment Design
|
||||
Make good habits easy and bad habits hard.
|
||||
|
||||
*** Applied Techniques
|
||||
- Morning routine checklist
|
||||
- Reading time after coffee
|
||||
- Phone in separate room while working
|
||||
|
||||
* Reading Goals
|
||||
** 2024 Targets
|
||||
- [ ] 24 books total (2 per month)
|
||||
- [ ] 60% technical, 40% non-technical
|
||||
- [ ] At least 4 books on system design
|
||||
- [ ] 2 biographies of technology leaders`,
|
||||
|
||||
'web-development.org': `#+TITLE: Web Development Reference
|
||||
#+DESCRIPTION: Quick reference for web development concepts
|
||||
#+KEYWORDS: javascript vue css html
|
||||
|
||||
* Frontend Technologies
|
||||
** JavaScript
|
||||
*** ES6+ Features
|
||||
**** Arrow Functions
|
||||
Arrow functions provide a concise way to write function expressions:
|
||||
\`\`\`javascript
|
||||
const add = (a, b) => a + b;
|
||||
const users = data.map(user => user.name);
|
||||
\`\`\`
|
||||
|
||||
**** Destructuring
|
||||
Extract values from arrays and objects:
|
||||
\`\`\`javascript
|
||||
const {name, age} = person;
|
||||
const [first, second] = array;
|
||||
\`\`\`
|
||||
|
||||
*** Async Programming
|
||||
**** Promises
|
||||
Handle asynchronous operations cleanly.
|
||||
|
||||
**** Async/Await
|
||||
Modern syntax for working with promises.
|
||||
|
||||
** Vue.js
|
||||
*** Composition API
|
||||
The modern way to write Vue components.
|
||||
|
||||
**** Setup Function
|
||||
The setup function is the entry point for Composition API.
|
||||
|
||||
*** Reactivity
|
||||
**** Ref vs Reactive
|
||||
- ref() for primitive values
|
||||
- reactive() for objects
|
||||
|
||||
** CSS
|
||||
*** Flexbox
|
||||
**** Container Properties
|
||||
- display: flex
|
||||
- flex-direction
|
||||
- justify-content
|
||||
- align-items
|
||||
|
||||
**** Item Properties
|
||||
- flex-grow
|
||||
- flex-shrink
|
||||
- flex-basis
|
||||
|
||||
*** Grid
|
||||
Modern layout system for complex designs.
|
||||
|
||||
* Backend Technologies
|
||||
** Node.js
|
||||
*** Express Framework
|
||||
Minimal web framework for Node.js.
|
||||
|
||||
**** Basic Server
|
||||
Simple Express server setup.
|
||||
|
||||
** Databases
|
||||
*** SQL
|
||||
**** Common Queries
|
||||
***** SELECT
|
||||
Basic query to retrieve data.
|
||||
|
||||
***** JOIN
|
||||
Combine data from multiple tables.
|
||||
|
||||
*** NoSQL
|
||||
**** MongoDB
|
||||
Document-based database with flexible schema.
|
||||
|
||||
* Tools and Workflow
|
||||
** Development Environment
|
||||
*** VS Code Extensions
|
||||
- Vetur for Vue development
|
||||
- Prettier for code formatting
|
||||
- ESLint for code quality
|
||||
|
||||
** Version Control
|
||||
*** Git Commands
|
||||
**** Basic Workflow
|
||||
Standard git workflow for daily development.
|
||||
|
||||
**** Branching
|
||||
Create and manage feature branches.
|
||||
|
||||
** Build Tools
|
||||
*** Vite
|
||||
Modern build tool with fast HMR (Hot Module Replacement).`
|
||||
}
|
||||
20
src/main.ts
Normal file
20
src/main.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import App from './App.vue'
|
||||
import HomeView from './views/HomeView.vue'
|
||||
import './style.css'
|
||||
|
||||
const routes = [
|
||||
{ path: '/', name: 'home', component: HomeView }
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes
|
||||
})
|
||||
|
||||
const app = createApp(App)
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
app.mount('#app')
|
||||
3
src/style.css
Normal file
3
src/style.css
Normal file
@@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
25
src/types/org.ts
Normal file
25
src/types/org.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
export interface OrgNode {
|
||||
id: string
|
||||
level: number
|
||||
title: string
|
||||
content: string
|
||||
children: OrgNode[]
|
||||
properties?: Record<string, string>
|
||||
tags?: string[]
|
||||
}
|
||||
|
||||
export interface OrgFile {
|
||||
id: string
|
||||
name: string
|
||||
path: string
|
||||
content: string
|
||||
nodes: OrgNode[]
|
||||
metadata?: Record<string, string>
|
||||
}
|
||||
|
||||
export interface FileSystemEntry {
|
||||
name: string
|
||||
path: string
|
||||
isDirectory: boolean
|
||||
children?: FileSystemEntry[]
|
||||
}
|
||||
47
src/utils/orgGenerator.ts
Normal file
47
src/utils/orgGenerator.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import type { OrgFile, OrgNode } from '@/types/org'
|
||||
|
||||
export function generateOrgContent(file: OrgFile): string {
|
||||
let content = ''
|
||||
|
||||
// Add metadata
|
||||
if (file.metadata) {
|
||||
for (const [key, value] of Object.entries(file.metadata)) {
|
||||
content += `#+${key}: ${value}\n`
|
||||
}
|
||||
content += '\n'
|
||||
}
|
||||
|
||||
// Add nodes
|
||||
for (const node of file.nodes) {
|
||||
content += generateNodeContent(node)
|
||||
}
|
||||
|
||||
return content.trim()
|
||||
}
|
||||
|
||||
function generateNodeContent(node: OrgNode): string {
|
||||
let content = ''
|
||||
|
||||
// Generate heading
|
||||
const stars = '*'.repeat(node.level)
|
||||
let heading = `${stars} ${node.title}`
|
||||
|
||||
// Add tags
|
||||
if (node.tags && node.tags.length > 0) {
|
||||
heading += ` :${node.tags.join(':')}:`
|
||||
}
|
||||
|
||||
content += heading + '\n'
|
||||
|
||||
// Add content
|
||||
if (node.content) {
|
||||
content += node.content + '\n'
|
||||
}
|
||||
|
||||
// Add children
|
||||
for (const child of node.children) {
|
||||
content += generateNodeContent(child)
|
||||
}
|
||||
|
||||
return content
|
||||
}
|
||||
105
src/utils/orgParser.ts
Normal file
105
src/utils/orgParser.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import type { OrgFile, OrgNode } from '@/types/org'
|
||||
|
||||
export function parseOrgFile(filename: string, content: string): OrgFile {
|
||||
const lines = content.split('\n')
|
||||
const nodes: OrgNode[] = []
|
||||
const metadata: Record<string, string> = {}
|
||||
|
||||
let currentNode: Partial<OrgNode> | null = null
|
||||
let nodeCounter = 0
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i]
|
||||
|
||||
// Parse metadata
|
||||
if (line.startsWith('#+')) {
|
||||
const [key, ...valueParts] = line.slice(2).split(':')
|
||||
if (valueParts.length > 0) {
|
||||
metadata[key.trim()] = valueParts.join(':').trim()
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse headings
|
||||
const headingMatch = line.match(/^(\*+)\s+(.+)$/)
|
||||
if (headingMatch) {
|
||||
// Save previous node
|
||||
if (currentNode) {
|
||||
nodes.push(finalizeNode(currentNode))
|
||||
}
|
||||
|
||||
const level = headingMatch[1].length
|
||||
const titleWithTags = headingMatch[2]
|
||||
|
||||
// Extract tags
|
||||
const tagMatch = titleWithTags.match(/^(.+?)\s+:([\w:]+):$/)
|
||||
const title = tagMatch ? tagMatch[1].trim() : titleWithTags.trim()
|
||||
const tags = tagMatch ? tagMatch[2].split(':').filter(Boolean) : []
|
||||
|
||||
currentNode = {
|
||||
id: `node-${nodeCounter++}`,
|
||||
level,
|
||||
title,
|
||||
content: '',
|
||||
children: [],
|
||||
tags
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Add content to current node
|
||||
if (currentNode && line.trim()) {
|
||||
currentNode.content = (currentNode.content || '') + line + '\n'
|
||||
}
|
||||
}
|
||||
|
||||
// Save last node
|
||||
if (currentNode) {
|
||||
nodes.push(finalizeNode(currentNode))
|
||||
}
|
||||
|
||||
// Build hierarchy
|
||||
const hierarchicalNodes = buildHierarchy(nodes)
|
||||
|
||||
return {
|
||||
id: `file-${Date.now()}`,
|
||||
name: filename,
|
||||
path: filename,
|
||||
content,
|
||||
nodes: hierarchicalNodes,
|
||||
metadata
|
||||
}
|
||||
}
|
||||
|
||||
function finalizeNode(partial: Partial<OrgNode>): OrgNode {
|
||||
return {
|
||||
id: partial.id!,
|
||||
level: partial.level!,
|
||||
title: partial.title!,
|
||||
content: (partial.content || '').trim(),
|
||||
children: [],
|
||||
tags: partial.tags
|
||||
}
|
||||
}
|
||||
|
||||
function buildHierarchy(flatNodes: OrgNode[]): OrgNode[] {
|
||||
const result: OrgNode[] = []
|
||||
const stack: OrgNode[] = []
|
||||
|
||||
for (const node of flatNodes) {
|
||||
// Pop stack until we find appropriate parent
|
||||
while (stack.length > 0 && stack[stack.length - 1].level >= node.level) {
|
||||
stack.pop()
|
||||
}
|
||||
|
||||
if (stack.length === 0) {
|
||||
result.push(node)
|
||||
} else {
|
||||
stack[stack.length - 1].children.push(node)
|
||||
}
|
||||
|
||||
stack.push(node)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
37
src/views/HomeView.vue
Normal file
37
src/views/HomeView.vue
Normal file
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<div class="container mx-auto p-6">
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<FileUploader @files-selected="handleFiles" />
|
||||
<div v-if="files.length" class="mt-8">
|
||||
<FileTabs
|
||||
:files="files"
|
||||
:active-file="activeFile"
|
||||
@file-selected="setActiveFile"
|
||||
/>
|
||||
<OrgTreeView v-if="activeFile" :file="activeFile" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import FileUploader from '@/components/FileManager/FileUploader.vue'
|
||||
import FileTabs from '@/components/FileManager/FileTabs.vue'
|
||||
import OrgTreeView from '@/components/OrgTree/OrgTreeView.vue'
|
||||
import type { OrgFile } from '@/types/org'
|
||||
|
||||
const files = ref<OrgFile[]>([])
|
||||
const activeFile = ref<OrgFile | null>(null)
|
||||
|
||||
function handleFiles(uploadedFiles: OrgFile[]) {
|
||||
files.value = uploadedFiles
|
||||
if (uploadedFiles.length > 0) {
|
||||
activeFile.value = uploadedFiles[0]
|
||||
}
|
||||
}
|
||||
|
||||
function setActiveFile(file: OrgFile) {
|
||||
activeFile.value = file
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user