Documentation · v0.2.0

Ό,τι χρειάζεσαι για να χτίσεις.

Από ένα script tag μέχρι server-side rendering σε PHP και Python. Ο editor, το core και τα SDKs μοιράζονται το ίδιο writer-ir.v1 contract — οπότε οι έννοιες είναι ίδιες παντού.

writer-ir.v1 32 fixtures JS/TS · PHP · Python MIT

Εισαγωγή

Ο Writer είναι τρία layers που μοιράζονται ένα contract:

  • Editor — Web Components (<writer-editor> και companion elements) με WYSIWYG, source, split, preview και readonly modes.
  • Core — @writer/core: Markdown → Writer IR, plugins, renderers, τυπογραφία, πίνακες, media, commands.
  • SDKs — writer/writer (PHP) και writer (Python) που παράγουν το ίδιο canonical IR και τις ίδιες εξόδους.

Το κέντρο βάρους είναι το Writer IR: ένα σταθερό, κανονικοποιημένο JSON δέντρο. Ο editor γράφει Markdown και IR· οι renderers διαβάζουν IR. Δεν υπάρχει “HTML που ελπίζουμε να ξαναγίνει Markdown”.

Frozen contract. Το writer-ir.v1 είναι το σημείο αναφοράς. Αλλαγές στο σχήμα περνούν πρώτα από το spec και τα 32 conformance fixtures — μετά από τον κώδικα.
Νέο στη v0.2.0. Το raw HTML sanitize-άρεται στο IR boundary σε JS, PHP και Python. Το @writer/pdf παράγει πραγματικά PDF bytes με ελληνικούς χαρακτήρες, και τα React/Vue/Svelte wrappers είναι έτοιμα. Δες το changelog.

Πακέτα και εγκατάσταση

ΠλατφόρμαΠακέτοΕγκατάσταση
JS / TS@writer/core
@writer/editor
npm install @writer/core @writer/editor
PHPwriter/writercomposer require writer/writer
Pythonwriterpip install writer
CDNself-hosted bundle<script src="/assets/writer/writer.min.js"></script>

Τα bundles είναι στο /assets/writer/. Κατέβασε το ZIP ή μεμονωμένα αρχεία από τη σελίδα Download.

Quickstart

Διάλεξε περιβάλλον. Το αποτέλεσμα είναι το ίδιο document, είτε το render κάνει ο browser είτε ο server.

index.html
<link rel="stylesheet" href="https://writer.gr/assets/writer/writer.css">
<script src="https://writer.gr/assets/writer/writer.min.js"></script>

<writer-editor
  id="editor"
  theme="serif-editorial"
  placeholder="Γράψε…"
></writer-editor>

<script>
  const editor = document.querySelector('#editor');
  editor.value = '# Hello\n';

  editor.addEventListener('wr-change', () => {
    console.log(editor.value); // Markdown
  });
</script>

Το writer.css είναι προαιρετικό page-level token sheet. Αν το παραλείψεις, όρισε τα --wr-* tokens στο host.

Ένα document, πολλές έξοδοι. Το ίδιο Markdown δίνει το ίδιο IR σε JS, PHP και Python. Το conformance suite το ελέγχει σε 32 fixtures × 3 γλώσσες.

Editor

Ο editor είναι ένα custom element. Δεν χρειάζεται React/Vue wrapper: το value είναι Markdown in, Markdown out, και το editor.writer σου δίνει ολόκληρο το core.

editor.html
<writer-editor
  id="editor"
  mode="wysiwyg"
  theme="serif-editorial"
  scheme="light"
  locale="el"
  placeholder="Γράψε…"
  toolbar="true"
  statusbar="true"
></writer-editor>

Modes: wysiwyg, source, split, preview, readonly. Σε WYSIWYG τα commands εφαρμόζονται στο rendered DOM και ξαναγράφονται σε Markdown· σε source/split/preview δουλεύουν πάνω στο Markdown source.

Attributes

AttributeΤιμέςΣημείωση
valueMarkdownΔύο κατευθύνσεων· διάβασε την property value.
modewysiwyg · source · split · preview · readonlyDefault wysiwyg.
themedefault · serif-editorial · technical · compact · magazineTypography theme.
schemelight · darkΑν λείπει, ακολουθεί το prefers-color-scheme.
readonly / disabledbooleanReadonly κρατά selection/rendering· disabled είναι πλήρως inert.
toolbar / statusbarbooleanDefault on όταν λείπουν. Το string "false" τα κρύβει.
outlinebooleanDefault off· η παρουσία το ενεργοποιεί.
media-providerprovider idΠοιος provider τροφοδοτεί το media manager.
media-endpoint / media-tokenURL / stringWriter Media Protocol provider + bearer token.
locale / directionBCP-47 / ltr · rtlΓλώσσα και κατεύθυνση.

Methods

MethodΤι κάνει
setMarkdown(md) / getMarkdown()Markdown in / out.
setHTML(html) / getHTML()Ρεντάρει το τρέχον document (ή θέτει HTML).
exec(id, ...args)Τρέχει οποιοδήποτε command (π.χ. format.bold, table.insert).
isCommandActive(id)Pressed-state του τρέχοντος selection.
canRun(id)Contextual διαθεσιμότητα (π.χ. format.unlink εκτός link).
setMode(mode) / setTheme(name)Αλλαγή mode / τυπογραφίας.
openPanel('table' | 'typography' | 'css' | 'media')Ανοίγει overlay panel στο top layer.
closePanels()Κλείνει όλα τα panels.
openMediaManager(opts?)Media picker· το insert γίνεται στο caret που θυμήθηκε.
insertMedia(entry, opts?)Εισάγει entry και επιστρέφει <img> σε WYSIWYG.
registerPlugin(plugin)Προσθέτει plugin στο τρέχον engine.
destroy()Καθαρίζει listeners και DOM.
Chrome defaults. Επειδή toolbar/statusbar είναι on όταν λείπουν, μην αφαιρείς το attribute για να τα κρύψεις — βάλε toolbar="false" και κάλεσε rebuildChrome() αν το αλλάζεις runtime.

Events

Eventdetail
wr-ready{ writer }
wr-input{ value, document } — αλλαγή από τον χρήστη.
wr-change{ source, document, entry? } — κάθε αλλαγή, και programmatic.
wr-mode-change{ mode }
wr-selection{ selection }
wr-command{ id, result }
wr-media-select{ entries } — commit μιας επιλογής.
wr-upload-progress{ name, loaded, total }
wr-error{ error }

Όλα τα events bubble και είναι composed, οπότε φτάνουν έξω από το shadow root.

Core API

Το createWriter() δίνει ένα stateful engine. Το WriterRender() είναι το one-shot, chainable builder για server-side ή static rendering.

core.ts
import { createWriter, parse, stringify } from '@writer/core';

const writer = createWriter({
  markdown: '# Hello\n',
  gfm: true,
  html: 'sanitize',
  typography: 'serif-editorial',
});

writer.toHTML();          // HTML string
writer.toIR();            // WriterNode (document)
writer.toJSON(true);      // pretty JSON
writer.toMarkdown();      // normalized Markdown
writer.toText();          // plain text

const ir = parse('**hi**');        // Markdown → IR
const md = stringify(ir);          // IR → Markdown

Το createWriter() δέχεται markdown, document (IR, κερδίζει το markdown), gfm, html (sanitize | allow | escape), breaks, linkify, plugins, typography, css, media, renderers, baseUrl.

WriterRender

Chainable builder με την ίδια εργονομία σε JS, PHP και Python. Δεν κρατά state· κάθε κλήση παράγει ένα αποτέλεσμα.

render.ts
import { WriterRender } from '@writer/core';

const html = WriterRender(markdown)
  .theme('serif-editorial')
  .variables({ '--wr-content-max-width': '60rem' })
  .asHTML();

const ir   = WriterRender(markdown).asIR();
const json = WriterRender(markdown).asJSON({}, true);
const md   = WriterRender(markdown).asMarkdown();
const text = WriterRender(markdown).asText();
const pdf  = await WriterRender(markdown).asPDF();
MethodΕπιστρέφει
asHTML(options?)string
asIR(options?)WriterNode
asJSON(options?, pretty?)string
asMarkdown(options?) / asText(options?)string
asPDF(options?)Promise<RenderResult>
as(format, options?)Promise<RenderResult>
theme(name) / variables(vars) / use(plugin)chainable RenderBuilder
asHTMLSync() / asIRSync() / asJSONSync(pretty?)sync variants (JS)

Ένα RenderResult περιέχει format, contents (text ή base64 για binary), binary, mime, bytes, assets και warnings.

Writer IR

Το IR είναι ένα καθαρό JSON δέντρο. Οι θέσεις (positions) δεν είναι μέρος του contract· το normalize() τις αφαιρεί, ενώνει γειτονικά text nodes και κανονικοποιεί whitespace.

writer-ir.v1
{
  "type": "document",
  "children": [
    {
      "type": "heading",
      "depth": 2,
      "children": [{ "type": "text", "value": "Hello" }]
    },
    {
      "type": "paragraph",
      "children": [
        { "type": "text", "value": "Use " },
        { "type": "strong", "children": [{ "type": "text", "value": "bold" }] },
        { "type": "text", "value": "." }
      ]
    }
  ]
}
  • Τα container nodes έχουν πάντα children (ίσως κενό).
  • Τα “always present” πεδία (link.title, image.alt) εκπέμπονται ακόμα και null/κενά.
  • Τα προαιρετικά (id, classes, attrs, callout, checked) παραλείπονται αντί να γίνονται null.
  • Εγγύηση round-trip: parse(stringify(parse(md))) ≡ normalize(parse(md)).

Τυπογραφία

Πέντε built-in themes: default, serif-editorial, technical, compact, magazine. Η ίδια ρύθμιση οδηγεί editor, HTML και print HTML.

typography.ts
import { createWriter, THEMES } from '@writer/core';

const writer = createWriter({ typography: 'serif-editorial' });

writer.typography.setTheme('technical');
const vars = writer.typography.variables();
const css  = writer.typography.toCss('article');
const scale = writer.typography.scale();

console.log(THEMES); // default, serif-editorial, technical, compact, magazine

Τα tokens είναι CSS custom properties και περνούν το shadow boundary του editor:

theme.css
writer-editor {
  --wr-font-sans: "Inter", system-ui, sans-serif;
  --wr-color-accent: #0b5fd7;
  --wr-content-max-width: 62rem;
  --wr-radius: 10px;
}

Dark mode: scheme="dark", theme="… dark" ή prefers-color-scheme. Το dark scheme κρατά την ιδιοκτησία του text color, ώστε κάθε theme να παραμένει ευανάγνωστο.

CSS frameworks

Ο adapter αναγνωρίζει Bootstrap, Tailwind, Bulma, Foundation και UIkit — και ξέρει τι σημαίνει ένα class.

css.ts
const matches = writer.css.detect({
  classes: ['container', 'row', 'col-md-6']
});

const classes = writer.css.gridClasses(
  { columns: 12, framework: 'bootstrap' },
  'bootstrap'
);

const container = writer.css.buildGrid({
  columns: 12,
  framework: 'bootstrap',
  rows: [[
    { content: 'A', span: 6 },
    { content: 'B', span: 6 }
  ]]
});

const issues = writer.css.lint(writer.doc);

detect() επιστρέφει matches με confidence/evidence, classify() χωρίζει grid/utilities/components, buildGrid() επιστρέφει container IR node και lint() επιστρέφει issues με severity και fix hint.

Πίνακες

Το TableEngine είναι πλήρες: δημιουργία, CSV/TSV, rows/columns, alignment, header, transpose. Όλοι οι mutators είναι pure — επιστρέφουν νέο IR δέντρο.

tables.ts
const table = writer.tables.create({
  rows: 2,
  columns: 3,
  header: true
});

const wider = writer.tables.insertColumn(table, 1);
const csv   = writer.tables.toCsv(wider);

writer.apply(() => writer.tables.setAlign(wider, 2, 'right'));

const session = writer.tables.session(table.id);
session.focusCell(0, 1);

Η TableSession είναι δεμένη με το identity του πίνακα και επιβιώνει από re-parsing — έτσι το focused cell μένει σταθερό όσο πληκτρολογείς σε WYSIWYG.

Media

Ο editor δεν ξέρει από filesystem. Ξέρει από providers: μικρά objects με list, stat, read, url και προαιρετικά write/upload/delete/folders.

media.ts
import {
  createWriter,
  createMemoryProvider,
  createServerProvider,
} from '@writer/core';

const writer = createWriter();

writer.media.register(createMemoryProvider({ '/logo.svg': '<svg/>' }, 'demo'));

writer.media.register(createServerProvider({
  id: 'cdn',
  endpoint: 'https://api.example.com/media',
  remote: 'assets',
  auth: async () => ({ Authorization: `Bearer ${await getToken()}` }),
}));

writer.media.use('cdn');

Built-in providers (JS)

FactoryKindΠρόσβασηΧρήση
createMemoryProvider()memoryfullTests, demos, offline.
createHttpProvider()httpread-onlyJSON index + HEAD.
createServerProvider()serverfullWriter Media Protocol → PHP/Python/Node.

FTP, SFTP, S3 και cloud providers ζουν στα server SDKs. Ο browser τους φτάνει μέσω του protocol — ποτέ με credentials στο client.

Standalone component

Το <writer-media-manager> δουλεύει και έξω από τον editor. Δώσε του έναν writer (ή endpoint) και κάνει browse, upload, rename, duplicate, delete.

media.html
<writer-media-manager
  open
  provider="cdn"
  path="/uploads"
  accept="image/*"
  multiple
  title="Media library"
></writer-media-manager>
media.ts
const manager = document.querySelector('writer-media-manager');

manager.writer = createWriter({ media: [provider] });

manager.addEventListener('wr-media-select', (event) => {
  const [entry] = event.detail.entries;
  const markdown = manager.writer.media.toMarkdown(entry, {
    alt: entry.name,
  });
  console.log(markdown);
});

Το upload δεν κάνει insert: ανανεώνει τη λίστα, επιλέγει τα νέα αρχεία και περιμένει Insert. Σε single select, η επιλογή κλείνει το panel και επιστρέφει το entry.

Writer Media Protocol

Ένα μικρό REST contract. Το PHP Writer\Server\MediaEndpoint και το Python writer.server.media_router το υλοποιούν, οπότε ο ίδιος browser κώδικας μιλά σε οποιοδήποτε backend.

protocol
GET    {base}?action=list&provider={id}&path={path}
GET    {base}?action=stat&provider={id}&path={path}
GET    {base}?action=url&provider={id}&path={path}
GET    {base}?action=read&provider={id}&path={path}
GET    {base}?action=search&provider={id}&q={query}
POST   {base}?action=upload&provider={id}&path={path}
POST   {base}?action=mkdir&provider={id}&path={path}
POST   {base}?action=move&provider={id}&from={p}&to={p}
POST   {base}?action=copy&provider={id}&from={p}&to={p}
DELETE {base}?action=delete&provider={id}&path={path}
media.php
<?php
use Writer\Media\MediaManager;
use Writer\Media\LocalProvider;
use Writer\Server\MediaEndpoint;

$media = new MediaManager();
$media->register(new LocalProvider('/var/www/uploads', 'uploads'));

$endpoint = new MediaEndpoint($media, $responseFactory, [
    'tokens' => ['secret-bearer-token'],
]);

$response = $endpoint->handle($request);
media.py
from writer.media import MediaManager, LocalProvider
from writer.server import media_router

media = MediaManager()
media.register(LocalProvider("/var/www/uploads", "uploads"))

router = media_router(media, tokens=["secret-bearer-token"])
Credentials. Μην στέλνεις long-lived S3/FTP keys στον browser. Κάνε register τον provider στον server και δώσε στον client μόνο ένα bearer token ή ένα auth callback.

PDF

Το PDF είναι opt-in και ειλικρινές. Στο JS το @writer/pdf δίνει first-party renderer με ενσωματωμένες γραμματοσειρές· εναλλακτικά engine print/server/custom. Χωρίς renderer ή engine, το asPDF() αποτυγχάνει με NO_PDF_ENGINE — δεν επιστρέφει HTML με κατάληξη .pdf.

ΓλώσσαΣυμπεριφορά
JS / TSPromise<RenderResult>· @writer/pdf renderer, engine print, server ή custom. Χωρίς ένα από αυτά → NO_PDF_ENGINE.
PHPRaw bytes· DompdfEngine όταν υπάρχει dompdf/dompdf, αλλιώς PrintEngine για print-ready HTML.
PythonRaw %PDF-…%%EOF bytes μέσω ReportLab.
pdf.ts
try {
  const result = await WriterRender(markdown).asPDF({
    page: { size: 'A4' },
    metadata: { title: 'Report' },
  });
  // result.contents: base64 PDF
} catch (error) {
  if (error.code === 'NO_PDF_ENGINE') {
    // Fallback: standalone print HTML
    const print = WriterRender(markdown).asHTML({
      standalone: true,
      embedCss: true,
    });
  }
}
pdf.php
$pdf = WriterRender($markdown)->asPDF();

header('Content-Type: application/pdf');
header('Content-Disposition: attachment; filename="document.pdf"');
echo $pdf;
pdf.py
pdf = WriterRender(markdown).as_pdf()
# bytes — ξεκινούν με b"%PDF-"

with open("document.pdf", "wb") as handle:
    handle.write(pdf)

Στη σελίδα Demos μπορείς να δεις το πραγματικό NO_PDF_ENGINE σφάλμα — δεν υπάρχει engine καταχωρημένος εκεί.

PHP

Το PHP SDK εκθέτει το ίδιο chainable WriterRender ως global function, μαζί με object API (Writer::create()) και providers για local/FTP/SFTP/S3.

render.php
<?php
require 'vendor/autoload.php';

use Writer\Writer;

$html = WriterRender($markdown)->asHTML();
$ir   = WriterRender($markdown)->asIR();
$json = WriterRender($markdown)->asJSON(true);
$md   = WriterRender($markdown)->asMarkdown();
$pdf  = WriterRender($markdown)->asPDF();

$writer = Writer::create(['gfm' => true, 'theme' => 'technical']);
$result = $writer->render('# Hi', 'html');
echo $result->contents();
MethodΕπιστρέφει
asHTML(array $options = [])string
asIR(array $options = [])array
asJSON(bool $pretty = false)string
asMarkdown() / asText()string
asPDF(array $options = [])raw PDF bytes
as(string $format, array $options = [])RenderResult
theme() / variables() / use() / withOptions()chainable self

Απαιτήσεις: PHP ≥ 8.1 με ext-dom. Προαιρετικά: dompdf/dompdf για πραγματικά PDF, ext-ftp, ext-ssh2, league/flysystem.

Python

Το Python SDK έχει snake_case μεθόδους και camelCase aliases για parity με το JS contract.

render.py
from writer import WriterRender, Writer, render

html = WriterRender("# Hello").as_html()
ir   = WriterRender("Hello *world*").as_ir()
json = WriterRender("# Hello").as_json(pretty=True)
md   = WriterRender("# Hello").as_markdown()
pdf  = WriterRender("# Hello").as_pdf()

result = Writer.render("# Hello", format="html")
same = render("# Hello", "html")

writer = Writer.create({"gfm": True, "theme": "technical"})
print(writer.to_html())

Απαιτήσεις: Python ≥ 3.9. Για PDF, ReportLab. Όλα τα δημόσια methods έχουν και camelCase alias (π.χ. asHTML → as_html).

Recipes

Editor στον browser, render στον server

Ο editor κρατά το Markdown· ο server το renderάρει με το ίδιο IR. Δεν χρειάζεται να στείλεις HTML από τον client.

client.ts
const markdown = editor.getMarkdown();

await fetch('/api/render', {
  method: 'POST',
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify({ markdown }),
});
server.php
<?php
require 'vendor/autoload.php';

$body = json_decode(file_get_contents('php://input'), true);
$markdown = $body['markdown'] ?? '';

header('Content-Type: text/html; charset=utf-8');
echo WriterRender($markdown)->asHTML([
    'standalone' => false,
]);

Draft snapshot

Το toJSONSnapshot() είναι serialisable και περιέχει version, irVersion, source, document και options.

snapshot.ts
const snapshot = writer.toJSONSnapshot();
localStorage.setItem('writer:draft', JSON.stringify(snapshot));

const saved = JSON.parse(localStorage.getItem('writer:draft'));
const restored = createWriter({ document: saved.document });

Custom command

command.ts
writer.commands.register({
  id: 'insert.signature',
  label: 'Insert signature',
  group: 'insert',
  run(ctx) {
    ctx.setSource(ctx.source + '\n\n— Writer');
    return true;
  },
});

Custom plugin

plugin.ts
import type { WriterPlugin } from '@writer/core';

const upper: WriterPlugin = {
  name: 'upper',
  render: {
    text(_ctx, output) {
      return String(output).toUpperCase();
    },
  },
};

const writer = createWriter({ plugins: [upper] });

Compatibility

Το ίδιο Markdown δίνει το ίδιο canonical IR σε JS, PHP και Python. Τα 32 fixtures του spec/ τρέχουν σε κάθε SDK.

FeatureJS/TSPHPPython
Parse Markdown → IR✓✓✓
Render IR → HTML✓✓✓
Render IR → Markdown✓✓✓
Render IR → PDF@writer/pdfdompdfReportLab
Typography manager✓✓✓
CSS framework adapter✓✓✓
Table engine✓✓✓
Media: local filesystem✓ (browser)✓✓
Media: FTP / SFTPvia server✓✓
Media: S3 / GCS / Azurevia server✓✓
Media Protocol endpointclient✓✓
Interactive editor✓——

Browsers: evergreen Chromium, Firefox και Safari. Ο editor χρειάζεται Custom Elements, Shadow DOM και <dialog>· τα bundles είναι ES2020.

Download

Πάρε το πλήρες πακέτο v0.2.0: browser bundles, render-only core, PHP και Python SDK sources και έτοιμα παραδείγματα. Άδεια MIT.

FileΜέγεθος (gzip)ΧρήσηΛήψη
writer.min.js≈130 kBEditor + core (script tag).Download
writer.jsunminifiedDebugging.Download
writer.cssμικρόΠροαιρετικά page-level tokens.Download
writer-core.min.js≈100 kBRender-only σελίδες (χωρίς editor).Download

Self-hosting των bundles

Σέρβιρε τα αρχεία από δικό σου path (π.χ. /assets/writer/0.2.0/) με μακρύ cache. Αν τα φορτώνεις από τρίτο domain, χρησιμοποίησε SRI:

index.html
<script
  src="/assets/writer/0.2.0/writer.min.js"
  integrity="sha384-…"
  crossorigin="anonymous"
></script>
  • MIME: text/javascript για .js, text/css για .css, image/svg+xml για .svg, application/manifest+json για .webmanifest.
  • CSP: το bundle δεν χρειάζεται unsafe-eval. Αν κόβεις inline scripts, βάλε το theme bootstrap σε εξωτερικό αρχείο.
  • Τα bundles είναι self-contained και δουλεύουν offline· δεν χρειάζονται framework runtime.
Τελευταία ενημέρωση: v0.2.0 · writer-ir.v1 Δες τα demos · Render lab · FAQ