Every creative project dies at the friction boundary—the point where switching between tools, formats, and contexts kills momentum. Writers abandon novels because Scrivener won't sync with their citation manager. Journalists miss deadlines wrestling with CMS formatting. Researchers lose weeks converting between Markdown, LaTeX, and Word. This guide presents a unified creative workspace architecture that handles books, articles, and media production from first draft to final export—without leaving the environment.
The Fragmentation Problem
Modern creative workflows demand a toolchain that looks like this:
Draft → Grammarly → Google Docs → Comments → Word →
Track Changes → InDesign → PDF → Email → Done?
vs.
Notes → Obsidian → Mermaid → Export → Figma →
Slides → Presentation → Export → Upload → Done?
vs.
Research → Zotero → LaTeX → Figures → Python →
Matplotlib → Overleaf → Journal → Revisions → Done?
Each arrow is context loss, format conversion, and cognitive overhead. The solution isn't better converters—it's a unified workspace where every format is a view onto the same underlying content model.
The Unified Content Model
At the heart of the workspace is a single, structured representation of your content:
# content_model.py
"""Unified content model supporting multiple output formats."""
from dataclasses import dataclass, field
from typing import List, Optional, Dict, Any
from enum import Enum
from pathlib import Path
import json
import yaml
class BlockType(Enum):
"""All possible content block types."""
HEADING = "heading"
PARAGRAPH = "paragraph"
CODE = "code"
QUOTE = "quote"
LIST = "list"
IMAGE = "image"
VIDEO = "video"
TABLE = "table"
DIAGRAM = "diagram"
MATH = "math"
CALLOUT = "callout"
BREAK = "break"
@dataclass
class InlineElement:
"""Inline formatting within text blocks."""
type: str # 'text', 'strong', 'em', 'code', 'link', 'footnote_ref'
content: str
metadata: Dict[str, Any] = field(default_factory=dict)
@dataclass
class ContentBlock:
"""A single block in the document."""
id: str
type: BlockType
content: List[InlineElement]
metadata: Dict[str, Any] = field(default_factory=dict)
children: List['ContentBlock'] = field(default_factory=list)
@classmethod
def heading(cls, level: int, text: str, id: Optional[str] = None) -> 'ContentBlock':
return cls(
id=id or f"heading-{level}-{hash(text) % 10000}",
type=BlockType.HEADING,
content=[InlineElement('text', text)],
metadata={'level': level}
)
@classmethod
def paragraph(cls, text: str, id: Optional[str] = None) -> 'ContentBlock':
return cls(
id=id or f"p-{hash(text) % 10000}",
type=BlockType.PARAGRAPH,
content=[InlineElement('text', text)]
)
@classmethod
def code(cls, language: str, source: str, id: Optional[str] = None) -> 'ContentBlock':
return cls(
id=id or f"code-{hash(source) % 10000}",
type=BlockType.CODE,
content=[InlineElement('text', source)],
metadata={'language': language, 'source': source}
)
@classmethod
def diagram(cls, diagram_type: str, source: str,
caption: str = "", id: Optional[str] = None) -> 'ContentBlock':
return cls(
id=id or f"diag-{hash(source) % 10000}",
type=BlockType.DIAGRAM,
content=[InlineElement('text', caption)],
metadata={
'diagram_type': diagram_type, # 'mermaid', 'plantuml', 'graphviz'
'source': source,
'caption': caption
}
)
@dataclass
class Document:
"""Root document containing all content and metadata."""
title: str
author: str
blocks: List[ContentBlock]
metadata: Dict[str, Any] = field(default_factory=dict)
# Cross-cutting concerns
footnotes: Dict[str, str] = field(default_factory=dict)
citations: List[Dict[str, str]] = field(default_factory=list)
media_files: List[Path] = field(default_factory=list)
def to_markdown(self) -> str:
"""Export to Markdown with extensions."""
parts = [f"# {self.title}\n\n"]
for block in self.blocks:
parts.append(self._block_to_markdown(block))
parts.append("\n\n")
# Append footnotes
for ref, text in self.footnotes.items():
parts.append(f"[^{ref}]: {text}\n")
return "".join(parts)
def _block_to_markdown(self, block: ContentBlock) -> str:
if block.type == BlockType.HEADING:
level = block.metadata.get('level', 1)
text = block.content[0].content if block.content else ""
return f"{'#' * level} {text}"
elif block.type == BlockType.PARAGRAPH:
return self._inline_to_markdown(block.content)
elif block.type == BlockType.CODE:
lang = block.metadata.get('language', '')
source = block.metadata.get('source', '')
return f"```{lang}\n{source}\n```"
elif block.type == BlockType.DIAGRAM:
dtype = block.metadata.get('diagram_type', 'mermaid')
source = block.metadata.get('source', '')
caption = block.metadata.get('caption', '')
return f"```{dtype}\n{source}\n```\n\n*{caption}*"
elif block.type == BlockType.QUOTE:
text = self._inline_to_markdown(block.content)
return "> " + text.replace("\n", "\n> ")
elif block.type == BlockType.CALLOUT:
kind = block.metadata.get('kind', 'info')
text = self._inline_to_markdown(block.content)
return f"> **{kind.upper()}**\n> {text}"
return ""
def _inline_to_markdown(self, elements: List[InlineElement]) -> str:
parts = []
for el in elements:
if el.type == 'text':
parts.append(el.content)
elif el.type == 'strong':
parts.append(f"**{el.content}**")
elif el.type == 'em':
parts.append(f"*{el.content}*")
elif el.type == 'code':
parts.append(f"`{el.content}`")
elif el.type == 'link':
url = el.metadata.get('url', '#')
parts.append(f"[{el.content}]({url})")
elif el.type == 'footnote_ref':
parts.append(f"[^{el.content}]")
return "".join(parts)
def to_html(self) -> str:
"""Export to HTML with semantic markup."""
# Implementation similar to Markdown but with HTML tags
# ... (omitted for brevity)
pass
def to_latex(self) -> str:
"""Export to LaTeX for academic publication."""
# Implementation with proper LaTeX escaping
# ... (omitted for brevity)
pass
def save(self, path: Path):
"""Serialize to JSON for persistence."""
data = {
'title': self.title,
'author': self.author,
'metadata': self.metadata,
'blocks': [
{
'id': b.id,
'type': b.type.value,
'content': [
{'type': e.type, 'content': e.content, 'metadata': e.metadata}
for e in b.content
],
'metadata': b.metadata,
'children': [] # Simplified
}
for b in self.blocks
],
'footnotes': self.footnotes,
'citations': self.citations
}
path.write_text(json.dumps(data, indent=2))
@classmethod
def load(cls, path: Path) -> 'Document':
"""Deserialize from JSON."""
data = json.loads(path.read_text())
blocks = []
for b in data['blocks']:
block = ContentBlock(
id=b['id'],
type=BlockType(b['type']),
content=[
InlineElement(e['type'], e['content'], e.get('metadata', {}))
for e in b['content']
],
metadata=b.get('metadata', {})
)
blocks.append(block)
return cls(
title=data['title'],
author=data['author'],
blocks=blocks,
metadata=data.get('metadata', {}),
footnotes=data.get('footnotes', {}),
citations=data.get('citations', [])
)
@classmethod
def from_markdown(cls, md_text: str, title: str = "Untitled",
author: str = "") -> 'Document':
"""Parse Markdown into structured document model."""
import re
blocks = []
lines = md_text.split('\n')
i = 0
while i < len(lines):
line = lines[i]
# Heading
heading_match = re.match(r'^(#{1,6})\s+(.+)$', line)
if heading_match:
level = len(heading_match.group(1))
text = heading_match.group(2)
blocks.append(ContentBlock.heading(level, text))
i += 1
continue
# Code block
if line.startswith('```'):
lang = line[3:].strip()
code_lines = []
i += 1
while i < len(lines) and not lines[i].startswith('```'):
code_lines.append(lines[i])
i += 1
i += 1 # Skip closing ```
blocks.append(ContentBlock.code(lang, '\n'.join(code_lines)))
continue
# Empty line
if not line.strip():
i += 1
continue
# Paragraph (collect until empty line)
para_lines = [line]
i += 1
while i < len(lines) and lines[i].strip():
para_lines.append(lines[i])
i += 1
blocks.append(ContentBlock.paragraph(' '.join(para_lines)))
return cls(title=title, author=author, blocks=blocks)
The Workspace Interface
With the content model in place, the workspace provides specialized views for different creative modes:
# workspace.py
"""Integrated creative workspace with multi-pane layout."""
import streamlit as st
from content_model import Document, ContentBlock, BlockType
from pathlib import Path
class CreativeWorkspace:
"""Streamlit-based creative workspace."""
def __init__(self):
st.set_page_config(
page_title="Tindol Creative Workspace",
layout="wide",
initial_sidebar_state="expanded"
)
self.doc = self._load_or_create_document()
def _load_or_create_document(self) -> Document:
"""Load existing document or create new."""
if 'document' not in st.session_state:
# Try to load from workspace directory
workspace_dir = Path.home() / '.tindol' / 'workspace'
workspace_dir.mkdir(parents=True, exist_ok=True)
# List existing documents
docs = list(workspace_dir.glob('*.json'))
if docs:
# Load most recent
latest = max(docs, key=lambda p: p.stat().st_mtime)
return Document.load(latest)
else:
# Create new
return Document(
title="New Document",
author="",
blocks=[]
)
return st.session_state['document']
def run(self):
"""Main workspace UI."""
self._sidebar()
self._main_content()
def _sidebar(self):
"""Navigation and metadata sidebar."""
with st.sidebar:
st.title("Tindol Workspace")
# Document metadata
self.doc.title = st.text_input("Title", self.doc.title)
self.doc.author = st.text_input("Author", self.doc.author)
st.divider()
# View mode
view_mode = st.radio("View", [
"Write", "Preview", "Outline", "Export"
])
st.divider()
# Block stats
st.caption(f"Blocks: {len(self.doc.blocks)}")
st.caption(f"Word count: {self._word_count()}")
# Save
if st.button("💾 Save"):
self._save()
st.success("Saved!")
def _main_content(self):
"""Main editing area."""
view_mode = st.session_state.get('view_mode', 'Write')
if view_mode == 'Write':
self._write_view()
elif view_mode == 'Preview':
self._preview_view()
elif view_mode == 'Outline':
self._outline_view()
elif view_mode == 'Export':
self._export_view()
def _write_view(self):
"""Block-based editor with live preview."""
st.header("Editor")
for i, block in enumerate(self.doc.blocks):
col1, col2 = st.columns([6, 1])
with col1:
if block.type == BlockType.HEADING:
level = block.metadata.get('level', 1)
text = st.text_input(
f"H{level}",
block.content[0].content if block.content else "",
key=f"h-{i}"
)
if text != (block.content[0].content if block.content else ""):
block.content = [type(block.content[0])('text', text)]
elif block.type == BlockType.PARAGRAPH:
text = st.text_area(
"Paragraph",
block.content[0].content if block.content else "",
key=f"p-{i}",
height=100
)
if text != (block.content[0].content if block.content else ""):
block.content = [type(block.content[0])('text', text)]
elif block.type == BlockType.CODE:
lang = st.text_input("Language",
block.metadata.get('language', 'python'),
key=f"lang-{i}"
)
source = st.text_area(
"Code",
block.metadata.get('source', ''),
key=f"code-{i}",
height=200
)
block.metadata['language'] = lang
block.metadata['source'] = source
elif block.type == BlockType.DIAGRAM:
dtype = st.selectbox(
"Type",
['mermaid', 'plantuml', 'graphviz'],
key=f"dtype-{i}"
)
source = st.text_area(
"Diagram Source",
block.metadata.get('source', ''),
key=f"diag-{i}",
height=200
)
block.metadata['diagram_type'] = dtype
block.metadata['source'] = source
with col2:
if st.button("🗑️", key=f"del-{i}"):
self.doc.blocks.pop(i)
st.rerun()
if st.button("⬆️", key=f"up-{i}") and i > 0:
self.doc.blocks[i-1], self.doc.blocks[i] = \
self.doc.blocks[i], self.doc.blocks[i-1]
st.rerun()
# Add new block
st.divider()
new_type = st.selectbox("Add block", [
'Paragraph', 'Heading', 'Code', 'Quote',
'Diagram', 'Callout', 'Break'
])
if st.button("➕ Add"):
if new_type == 'Heading':
self.doc.blocks.append(ContentBlock.heading(2, "New Section"))
elif new_type == 'Paragraph':
self.doc.blocks.append(ContentBlock.paragraph(""))
elif new_type == 'Code':
self.doc.blocks.append(ContentBlock.code('python', '# Your code here'))
elif new_type == 'Diagram':
self.doc.blocks.append(ContentBlock.diagram('mermaid',
'graph TD\n A[Start] --> B[End]'))
st.rerun()
def _preview_view(self):
"""Rendered preview of the document."""
st.header("Preview")
md = self.doc.to_markdown()
st.markdown(md, unsafe_allow_html=True)
def _outline_view(self):
"""Structural outline with drag-drop reordering."""
st.header("Outline")
for i, block in enumerate(self.doc.blocks):
if block.type == BlockType.HEADING:
level = block.metadata.get('level', 1)
text = block.content[0].content if block.content else ""
indent = " " * (level - 1)
st.write(f"{indent}**{text}**")
elif block.type in [BlockType.CODE, BlockType.DIAGRAM]:
text = block.metadata.get('language', block.metadata.get('diagram_type', 'block'))
st.write(f" `📎 {text}`")
def _export_view(self):
"""Export to multiple formats."""
st.header("Export")
col1, col2, col3 = st.columns(3)
with col1:
st.subheader("Markdown")
md = self.doc.to_markdown()
st.download_button(
"Download .md",
md,
file_name=f"{self.doc.title}.md",
mime="text/markdown"
)
st.text_area("Preview", md[:2000], height=300)
with col2:
st.subheader("LaTeX")
latex = self.doc.to_latex()
st.download_button(
"Download .tex",
latex,
file_name=f"{self.doc.title}.tex",
mime="application/x-tex"
)
with col3:
st.subheader("HTML")
html = self.doc.to_html()
st.download_button(
"Download .html",
html,
file_name=f"{self.doc.title}.html",
mime="text/html"
)
def _word_count(self) -> int:
"""Total word count across all text blocks."""
count = 0
for block in self.doc.blocks:
if block.content:
text = ' '.join(c.content for c in block.content)
count += len(text.split())
return count
def _save(self):
"""Persist document to disk."""
workspace_dir = Path.home() / '.tindol' / 'workspace'
workspace_dir.mkdir(parents=True, exist_ok=True)
filename = f"{self.doc.title.replace(' ', '_').lower()}.json"
path = workspace_dir / filename
self.doc.save(path)
st.session_state['document'] = self.doc
def main():
workspace = CreativeWorkspace()
workspace.run()
if __name__ == '__main__':
main()
Integrated Diagram Support
Visual thinking is inseparable from writing. The workspace embeds diagram rendering directly:
- Mermaid: Flowcharts, sequence diagrams, Gantt charts via JavaScript rendering
- PlantUML: Complex UML diagrams with server or local rendering
- Graphviz: Network graphs and hierarchical structures
- Matplotlib: Statistical figures from embedded Python code
Each diagram is stored as source text in the content model, rendered on demand, and regenerated when the source changes. This means your diagrams stay editable and version-controllable.
Export Pipeline
The unified model enables consistent exports across formats:
| Target | Best For | Key Considerations |
|---|---|---|
| Markdown | Blog posts, GitHub docs | Native compatibility, image path resolution |
| LaTeX | Academic papers, theses | Bibliography integration, math rendering |
| HTML | Web publications, newsletters | CSS styling, responsive design |
| EPUB | E-books, Kindle | Chapter splitting, TOC generation |
| Print, formal submission | Typography, pagination, margins |
The Bottom Line
The all-in-one creative workspace isn't about feature accumulation—it's about eliminating the friction between thinking and expressing. When your content model is rich enough to capture structure, formatting, media, and metadata; when your interface adapts to writing, editing, and reviewing modes; when your export pipeline handles any target format you need—then the tool disappears and only the work remains.
That's the goal: not a better word processor, but an environment where creative work flows naturally from inception to publication without the artificial boundaries imposed by file formats and application silos.