Compare commits

..

2 Commits

Author SHA1 Message Date
Tom Wright 93ca2b4966 Update build workflow 2020-08-09 11:44:25 +01:00
Tom Wright 7f9e6c91bd Update build workflow 2020-08-09 11:41:47 +01:00
40 changed files with 528 additions and 5751 deletions

View File

@ -1,18 +0,0 @@
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
- package-ecosystem: gomod
directory: /
schedule:
interval: weekly
- package-ecosystem: docker
directory: /
schedule:
interval: weekly
- package-ecosystem: npm
directory: /mermaidcli
schedule:
interval: weekly

View File

@ -7,16 +7,16 @@ jobs:
build:
strategy:
matrix:
go-version: [1.16.x]
go-version: [1.13.x]
platform: [ubuntu-latest]
runs-on: ${{ matrix.platform }}
steps:
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v1
- name: Set env
run: echo RELEASE_VERSION=${GITHUB_REF:10} >> $GITHUB_ENV
run: echo ::set-env name=RELEASE_VERSION::${GITHUB_REF:10}
- name: Build
run: docker build -t tomwright/mermaid-server:latest -t tomwright/mermaid-server:${{ env.RELEASE_VERSION }} -f Dockerfile .
run: docker build --ssh default -t tomwright/mermaid-server:latest -t tomwright/mermaid-server:${{ env.RELEASE_VERSION }} -f Dockerfile .
- name: Login
run: echo ${{ secrets.DOCKER_PASS }} | docker login -u${{ secrets.DOCKER_USER }} --password-stdin
- name: Push

View File

@ -4,17 +4,17 @@ jobs:
test:
strategy:
matrix:
go-version: [1.16.x]
go-version: [1.13.x]
platform: [ubuntu-latest]
runs-on: ${{ matrix.platform }}
steps:
- name: Install Go
uses: actions/setup-go@v3
uses: actions/setup-go@v1
with:
go-version: ${{ matrix.go-version }}
- name: Checkout code
uses: actions/checkout@v3
- uses: actions/cache@v3.0.10
uses: actions/checkout@v1
- uses: actions/cache@v1
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}

View File

@ -1,94 +1,77 @@
# Multi-stage build
# Stage 1: Build the Go executable
FROM golang:1.21-bookworm as go-builder
# This stage builds the go executable.
FROM golang:1.13-buster as go
WORKDIR /root
COPY go.mod go.sum ./
RUN go mod download
COPY ./ ./
COPY cmd/ ./cmd/
COPY internal/ ./internal/
RUN go build -o bin/app cmd/app/main.go
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o bin/app cmd/app/main.go
# Stage 2: Setup Node.js environment with mermaid CLI and Chrome
FROM node:20-bookworm-slim
# Final stage that will be pushed.
FROM debian:buster-slim
FROM node:12.12.0-buster-slim as node
WORKDIR /root
# copy the mermaidcli node package into the container and install
COPY ./mermaidcli/* ./
RUN npm install
# Install system dependencies for Chrome/Puppeteer
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y \
ca-certificates \
fonts-liberation \
libappindicator3-1 \
libasound2 \
libatk-bridge2.0-0 \
libatk1.0-0 \
libc6 \
libcairo2 \
libcups2 \
libdbus-1-3 \
libexpat1 \
libfontconfig1 \
libgbm1 \
libgcc1 \
libglib2.0-0 \
libgtk-3-0 \
libnspr4 \
libnss3 \
libpango-1.0-0 \
libpangocairo-1.0-0 \
libstdc++6 \
libx11-6 \
libx11-xcb1 \
libxcb1 \
libxcomposite1 \
libxcursor1 \
libxdamage1 \
libxext6 \
libxfixes3 \
libxi6 \
libxrandr2 \
libxrender1 \
libxss1 \
libxtst6 \
libxshmfence1 \
libdrm2 \
libxkbcommon0 \
libatspi2.0-0 \
lsb-release \
wget \
xdg-utils \
--no-install-recommends && \
rm -rf /var/lib/apt/lists/*
RUN apt-get update 2>/dev/null && \
apt-get install -y --no-install-recommends \
ca-certificates \
gconf-service \
libasound2 \
libatk1.0-0 \
libatk-bridge2.0-0 \
libc6 \
libcairo2 \
libcups2 \
libdbus-1-3 \
libexpat1 \
libfontconfig1 \
libgcc1 \
libgconf-2-4 \
libgdk-pixbuf2.0-0 \
libglib2.0-0 \
libgtk-3-0 \
libnspr4 \
libpango-1.0-0 \
libpangocairo-1.0-0 \
libstdc++6 \
libx11-6 \
libx11-xcb1 \
libxcb1 \
libxcomposite1 \
libxcursor1 \
libxdamage1 \
libxext6 \
libxfixes3 \
libxi6 \
libxrandr2 \
libxrender1 \
libxss1 \
libxtst6 \
libxcb-dri3-0 \
libgbm1 \
ca-certificates \
fonts-liberation \
libappindicator1 \
libnss3 \
lsb-release \
xdg-utils \
wget \
2>/dev/null
WORKDIR /root
COPY --from=go /root/bin/app ./app
# Copy Go executable
COPY --from=go-builder /root/bin/app ./app
RUN mkdir -p ./in
RUN mkdir -p ./out
RUN chmod 0777 ./in
RUN chmod 0777 ./out
# Copy mermaid CLI package files
COPY ./mermaidcli/package*.json ./
COPY ./mermaidcli/puppeteer-config.json ./
CMD ["./app", "--mermaid=./node_modules/.bin/mmdc", "--in=./in", "--out=./out", "--puppeteer=./puppeteer-config.json"]
# Install Node dependencies and Chrome in single layer
RUN npm ci --only=production && \
npx puppeteer browsers install chrome-headless-shell && \
npm cache clean --force
# Set environment variables - let Puppeteer find Chrome automatically
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=false
# Create directories for input/output
RUN mkdir -p ./in ./out && \
chmod 0777 ./in ./out
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:80/health || exit 1
# Expose port
EXPOSE 80
# Run the application directly
CMD ["./app", "--allow-all-origins=true", "--mermaid=./node_modules/.bin/mmdc", "--in=./in", "--out=./out", "--puppeteer=./puppeteer-config.json"]

21
LICENSE
View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2020 Tom Wright
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,21 +0,0 @@
DOCKER_IMAGE=tomwright/mermaid-server:latest
CONTAINER_NAME=mermaid-server
docker-image:
docker build -t ${DOCKER_IMAGE} .
docker-run:
docker run -d --name ${CONTAINER_NAME} -p 80:80 ${DOCKER_IMAGE}
docker-stop:
docker stop ${CONTAINER_NAME} || true
docker-rm:
make docker-stop
docker rm ${CONTAINER_NAME} || true
docker-logs:
docker logs -f ${CONTAINER_NAME}
docker-push:
docker push ${DOCKER_IMAGE}

View File

@ -22,8 +22,6 @@ go run cmd/app/main.go --mermaid=./mermaidcli/node_modules/.bin/mmdc --in=./in -
### Diagram creation
Use the query param 'type' to change between 'png' and 'svg' defaults to 'svg'.
#### POST
Send a CURL request to generate a diagram via `POST`:
@ -51,4 +49,4 @@ curl --location --request GET 'http://localhost:80/generate?data=graph%20LR%0A%0
### Caching
All generated diagram input and output will be cached for 1 hour. The cache time is reset whenever a cached diagram is accessed.

View File

@ -1,41 +0,0 @@
// Alternative API call using GET with URL encoding (like the healthcheck)
export const code = async (inputs) => {
const { diagram, format } = inputs;
if (!diagram || !diagram.trim()) {
throw new Error("No Mermaid input provided");
}
// Normalize line endings
const normalizedDiagram = diagram
.replace(/\r\n/g, '\n') // Convert Windows line endings
.replace(/\r/g, '\n') // Convert old Mac line endings
.trim(); // Remove leading/trailing whitespace
// URL encode the diagram (this preserves newlines as %0A)
const encodedDiagram = encodeURIComponent(normalizedDiagram);
// Log for debugging
console.log("Original diagram:");
console.log(JSON.stringify(normalizedDiagram));
console.log("URL encoded diagram:");
console.log(encodedDiagram);
// Use GET with data parameter (like the healthcheck does)
const url = `https://diagrams.starbit.cloud/generate?type=${encodeURIComponent(format)}&data=${encodedDiagram}`;
const resp = await fetch(url, {
method: "GET",
});
if (!resp.ok) {
const txt = await resp.text();
throw new Error(`Mermaid-server error ${resp.status}: ${txt}`);
}
const buffer = Buffer.from(await resp.arrayBuffer());
const mime = format === "png" ? "image/png" : "image/svg+xml";
const file = `data:${mime};base64,${buffer.toString("base64")}`;
return { file };
};

View File

@ -1,2 +0,0 @@
graph TD
A --> B

View File

@ -4,7 +4,7 @@ import (
"context"
"flag"
"fmt"
"github.com/tomwright/grace"
"github.com/tomwright/lifetime"
"github.com/tomwright/mermaid-server/internal"
"os"
)
@ -14,7 +14,6 @@ func main() {
in := flag.String("in", "", "Directory to store input files.")
out := flag.String("out", "", "Directory to store output files.")
puppeteer := flag.String("puppeteer", "", "Full path to optional puppeteer config.")
allowAllOrigins := flag.Bool("allow-all-origins", false, "True to allow all request origins")
flag.Parse()
if *mermaid == "" {
@ -32,16 +31,19 @@ func main() {
os.Exit(1)
}
g := grace.Init(context.Background())
cache := internal.NewDiagramCache()
generator := internal.NewGenerator(cache, *mermaid, *in, *out, *puppeteer)
httpRunner := internal.NewHTTPRunner(generator, *allowAllOrigins)
cleanupRunner := internal.NewCleanupRunner(generator)
httpService := internal.NewHTTPService(generator)
cleanupService := internal.NewCleanupService(generator)
g.Run(httpRunner)
g.Run(cleanupRunner)
lt := lifetime.New(context.Background()).Init()
g.Wait()
// Start the http service.
lt.Start(httpService)
// Start the cleanup service.
lt.Start(cleanupService)
// Wait for all routines to stop running.
lt.Wait()
}

View File

@ -1,65 +0,0 @@
// Debug script to test different approaches for quadrant charts
export const code = async (inputs) => {
const raw = String(inputs.diagram || "").trim();
if (!raw) throw new Error("No Mermaid input provided");
// Normalize line endings first
let normalizedDiagram = raw
.replace(/\r\n/g, '\n') // Convert Windows line endings
.replace(/\r/g, '\n'); // Convert old Mac line endings
console.log("Original diagram:");
console.log(normalizedDiagram);
// Check if it's already single line
if (!normalizedDiagram.includes('\n')) {
console.log("Already single line, returning as-is");
return { rawDsl: normalizedDiagram };
}
// For quadrant charts, try different encoding methods
const firstWord = normalizedDiagram.split(/\s+/)[0];
if (firstWord === 'quadrantChart') {
console.log("Quadrant chart detected - trying special encoding methods");
// Method 1: Try different newline encoding
const method1 = normalizedDiagram.replace(/\n/g, '\\n');
console.log("Method 1 (\\n encoding):", method1);
// Method 2: Try different marker
const method2 = normalizedDiagram.replace(/\n/g, ' | ');
console.log("Method 2 (pipe separator):", method2);
// Method 3: Try preserving original with different content-type hint
const method3 = `PRESERVE_NEWLINES:${normalizedDiagram}`;
console.log("Method 3 (preserve hint):", method3);
// Let's try method 2 first (pipe separator)
return { rawDsl: method2 };
}
// Convert other types to single line with semicolons
const convertibleTypes = ['graph', 'flowchart', 'sequenceDiagram', 'pie', 'gitGraph'];
if (convertibleTypes.some(type => firstWord.startsWith(type))) {
const lines = normalizedDiagram.split('\n');
const firstLine = lines[0];
const remainingLines = lines.slice(1)
.filter(line => line.trim())
.map(line => line.trim());
const singleLine = firstLine + '; ' + remainingLines.join('; ');
console.log("Converted to single line:");
console.log(singleLine);
return { rawDsl: singleLine };
}
// For other non-convertible types, use NEWLINE encoding
const encodedDiagram = normalizedDiagram.replace(/\n/g, ' NEWLINE ');
console.log("Using NEWLINE encoding:");
console.log(encodedDiagram);
return { rawDsl: encodedDiagram };
};

View File

@ -1,35 +0,0 @@
// Debug script to test what the server actually receives
const diagram = `graph TD
A[Test] --> B[Node]
A --> C[Another]`;
console.log("Original diagram:");
console.log(JSON.stringify(diagram));
console.log("\nDiagram visualization:");
console.log(diagram);
// Test with both your current format and a simple test
const testCases = [
{
name: "Simple test",
diagram: "graph TD\n A --> B"
},
{
name: "Your format",
diagram: diagram
},
{
name: "Minimal",
diagram: "graph TD\nA-->B"
}
];
testCases.forEach((testCase, i) => {
console.log(`\n=== Test Case ${i+1}: ${testCase.name} ===`);
console.log("JSON representation:", JSON.stringify(testCase.diagram));
console.log("Actual content:");
console.log(testCase.diagram);
console.log("Length:", testCase.diagram.length);
console.log("Contains newlines:", testCase.diagram.includes('\n'));
console.log("Newline positions:", [...testCase.diagram].map((char, idx) => char === '\n' ? idx : null).filter(x => x !== null));
});

View File

@ -1,20 +0,0 @@
version: "3.8"
services:
mermaid-server:
build: .
container_name: mermaid-server
ports:
- "7000:80" # Map host port 7000 to container port 80
volumes:
- ./in:/root/in # optional: persist input for caching
- ./out:/root/out # optional: persist output for debugging
environment:
ALLOW_ALL_ORIGINS: "true"
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:80/generate?data=graph%20TD%0A%20%20%20%20A%5BTest%5D"]
interval: 30s
timeout: 10s
start_period: 5s
retries: 3

View File

@ -1,23 +0,0 @@
// Enhanced mermaid conversion script that preserves newlines for all chart types
export const code = async (inputs) => {
const raw = String(inputs.diagram || "").trim();
if (!raw) throw new Error("No Mermaid input provided");
// Normalize line endings first
const normalizedDiagram = raw
.replace(/\r\n/g, '\n') // Convert Windows line endings
.replace(/\r/g, '\n'); // Convert old Mac line endings
// Base64 encode to preserve exact content including newlines
const base64Encoded = Buffer.from(normalizedDiagram, 'utf8').toString('base64');
// Create a wrapper that tells the server this is base64 encoded
const wrappedDiagram = `%%{init: {"base64": true}}%%\n${base64Encoded}`;
console.log("Original diagram length:", normalizedDiagram.length);
console.log("Original has newlines:", normalizedDiagram.includes('\n'));
console.log("Base64 encoded length:", base64Encoded.length);
console.log("Wrapped diagram:", wrappedDiagram);
return { rawDsl: wrappedDiagram };
};

View File

@ -1,47 +0,0 @@
// Final fixed API call - handles URL encoding edge cases properly
export const code = async (inputs) => {
const { diagram, format } = inputs;
if (!diagram || !diagram.trim()) {
throw new Error("No Mermaid input provided");
}
// Normalize line endings
const normalizedDiagram = diagram
.replace(/\r\n/g, '\n') // Convert Windows line endings
.replace(/\r/g, '\n') // Convert old Mac line endings
.trim(); // Remove leading/trailing whitespace
// Use URLSearchParams for proper encoding of complex content
const params = new URLSearchParams();
params.set('type', format);
params.set('data', normalizedDiagram);
// Log for debugging
console.log("Original diagram:");
console.log(JSON.stringify(normalizedDiagram));
console.log("URL params:");
console.log(params.toString());
// Build URL manually to avoid double encoding issues
const url = `https://diagrams.starbit.cloud/generate?${params.toString()}`;
const resp = await fetch(url, {
method: "GET",
headers: {
'Accept': 'image/svg+xml, image/png, */*',
'User-Agent': 'Mermaid-Client/1.0'
}
});
if (!resp.ok) {
const txt = await resp.text();
throw new Error(`Mermaid-server error ${resp.status}: ${txt}`);
}
const buffer = Buffer.from(await resp.arrayBuffer());
const mime = format === "png" ? "image/png" : "image/svg+xml";
const file = `data:${mime};base64,${buffer.toString("base64")}`;
return { file };
};

View File

@ -1,39 +0,0 @@
// Fixed API call script for your workflow
export const code = async (inputs) => {
const { diagram, format } = inputs;
// diagram is now your exact multiline DSL (including any %%init%% lines)
if (!diagram || !diagram.trim()) {
throw new Error("No Mermaid input provided");
}
// Ensure proper newlines are preserved and normalize line endings
const normalizedDiagram = diagram
.replace(/\r\n/g, '\n') // Convert Windows line endings
.replace(/\r/g, '\n') // Convert old Mac line endings
.trim(); // Remove leading/trailing whitespace
// Log for debugging
console.log("Sending diagram:");
console.log(JSON.stringify(normalizedDiagram));
const url = `https://diagrams.starbit.cloud/generate?type=${encodeURIComponent(format)}`;
const resp = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "text/plain; charset=utf-8" // Explicit charset
},
body: normalizedDiagram, // <-- send the normalized, multi-line DSL
});
if (!resp.ok) {
const txt = await resp.text();
throw new Error(`Mermaid-server error ${resp.status}: ${txt}`);
}
const buffer = Buffer.from(await resp.arrayBuffer());
const mime = format === "png" ? "image/png" : "image/svg+xml";
const file = `data:${mime};base64,${buffer.toString("base64")}`;
return { file };
};

7
go.mod
View File

@ -1,8 +1,5 @@
module github.com/tomwright/mermaid-server
go 1.21
go 1.13
require (
github.com/tomwright/grace v0.1.2
github.com/tomwright/gracehttpserverrunner v0.1.0
)
require github.com/tomwright/lifetime v1.0.0

6
go.sum
View File

@ -1,4 +1,2 @@
github.com/tomwright/grace v0.1.2 h1:8kH+S2GLqnwgWqUzi9CcjNoWJANZQnw9Xw65NPUr6WA=
github.com/tomwright/grace v0.1.2/go.mod h1:RKqz4gB3sQJpyas/CuiiriQQfUxSXhtWRfYtE7MG+Ok=
github.com/tomwright/gracehttpserverrunner v0.1.0 h1:n4iafOnJQEmRn05i1QzU+FPS0CU4ybxilyEdBEH/Ulk=
github.com/tomwright/gracehttpserverrunner v0.1.0/go.mod h1:FFHjVUgXu7KygMn+QlaoCesVlPOhaCnCvw35nvgzt5I=
github.com/tomwright/lifetime v1.0.0 h1:Yzj+Td38eUUdZ1ewvOegywFBmKyaCh+8HjKBmeXw6OM=
github.com/tomwright/lifetime v1.0.0/go.mod h1:GUCHgRaR/zStvtJiOd3B4gIZayeiz3TgApC9kNYAOQI=

View File

@ -1,46 +0,0 @@
package internal
import (
"context"
"github.com/tomwright/grace"
"log"
"time"
)
// NewCleanupRunner returns a runner that can be used cleanup old diagrams.
func NewCleanupRunner(generator Generator) grace.Runner {
return &cleanupService{
generator: generator,
runEvery: time.Minute * 5,
cleanupLast: time.Hour,
}
}
// cleanupService is a runner that is used cleanup old diagrams.
type cleanupService struct {
generator Generator
runEvery time.Duration
cleanupLast time.Duration
}
// Run starts the cleanup process.
func (s *cleanupService) Run(ctx context.Context) error {
for {
select {
case <-ctx.Done():
return nil
default:
}
if err := s.generator.CleanUp(s.cleanupLast); err != nil {
log.Printf("error when cleaning up: %s", err.Error())
}
select {
case <-time.After(s.runEvery):
continue
case <-ctx.Done():
return nil
}
}
}

View File

@ -0,0 +1,41 @@
package internal
import (
"log"
"time"
)
// NewCleanupService returns a service that can be used cleanup old diagrams.
func NewCleanupService(generator Generator) *cleanupService {
return &cleanupService{
generator: generator,
stopCh: make(chan struct{}),
}
}
// cleanupService is a service that can be used cleanup old diagrams.
type cleanupService struct {
generator Generator
stopCh chan struct{}
}
// Start starts the cleanup service.
func (s *cleanupService) Start() error {
for {
if err := s.generator.CleanUp(time.Hour); err != nil {
log.Printf("error when cleaning up: %s", err.Error())
}
select {
case <-time.After(time.Minute * 5):
continue
case <-s.stopCh:
return nil
}
}
}
// Stop stops the cleanup service.
func (s *cleanupService) Stop() {
close(s.stopCh)
}

View File

@ -4,42 +4,17 @@ import (
"crypto/md5"
"encoding/base64"
"encoding/hex"
"fmt"
"strings"
"sync"
"time"
)
// NewDiagram returns a new diagram.
func NewDiagram(description []byte, imgType string) *Diagram {
// Debug: Log what we received
fmt.Printf("DEBUG: Received %d bytes\n", len(description))
fmt.Printf("DEBUG: Raw bytes: %q\n", string(description))
fmt.Printf("DEBUG: Contains newlines: %t\n", strings.Contains(string(description), "\n"))
trimmed := strings.TrimSpace(string(description))
fmt.Printf("DEBUG: After TrimSpace: %q\n", trimmed)
fmt.Printf("DEBUG: Still contains newlines: %t\n", strings.Contains(trimmed, "\n"))
// Decode NEWLINE markers back to actual newlines for complex diagrams
if strings.Contains(trimmed, " NEWLINE ") {
trimmed = strings.ReplaceAll(trimmed, " NEWLINE ", "\n")
fmt.Printf("DEBUG: After NEWLINE decoding: %q\n", trimmed)
fmt.Printf("DEBUG: Now contains newlines: %t\n", strings.Contains(trimmed, "\n"))
}
// Decode pipe separators back to newlines for quadrant charts
if strings.Contains(trimmed, " | ") {
trimmed = strings.ReplaceAll(trimmed, " | ", "\n")
fmt.Printf("DEBUG: After pipe decoding: %q\n", trimmed)
fmt.Printf("DEBUG: Now contains newlines: %t\n", strings.Contains(trimmed, "\n"))
}
func NewDiagram(description []byte) *Diagram {
return &Diagram{
description: []byte(trimmed),
description: []byte(strings.TrimSpace(string(description))),
lastTouched: time.Now(),
mu: &sync.RWMutex{},
imgType: imgType,
}
}
@ -55,8 +30,6 @@ type Diagram struct {
mu *sync.RWMutex
// lastTouched is the time that the diagram was last used.
lastTouched time.Time
// the type of image to generate svg or png
imgType string
}
// Touch updates the last touched time of the diagram.
@ -82,7 +55,7 @@ func (d *Diagram) ID() (string, error) {
encoded := base64.StdEncoding.EncodeToString(d.description)
hash := md5.Sum([]byte(encoded))
d.id = hex.EncodeToString(hash[:]) + d.imgType
d.id = hex.EncodeToString(hash[:])
return d.id, nil
}

View File

@ -79,7 +79,7 @@ func (c cachingGenerator) generate(diagram *Diagram) error {
}
inPath := fmt.Sprintf("%s/%s.mmd", c.inPath, id)
outPath := fmt.Sprintf("%s/%s.%s", c.outPath, id, diagram.imgType)
outPath := fmt.Sprintf("%s/%s.svg", c.outPath, id)
if err := ioutil.WriteFile(inPath, diagram.description, 0644); err != nil {
return fmt.Errorf("could not write to input file [%s]: %w", inPath, err)

View File

@ -3,48 +3,13 @@ package internal
import (
"encoding/json"
"fmt"
"github.com/tomwright/grace"
"github.com/tomwright/gracehttpserverrunner"
"io/ioutil"
"log"
"net/http"
"net/url"
"strings"
"time"
)
// NewHTTPRunner returns a grace runner that runs a HTTP server.
func NewHTTPRunner(generator Generator, allowAllOrigins bool) grace.Runner {
httpHandler := generateHTTPHandler(generator)
if allowAllOrigins {
httpHandler = allowAllOriginsMiddleware(httpHandler)
}
r := http.NewServeMux()
r.Handle("/generate", httpHandler)
return &gracehttpserverrunner.HTTPServerRunner{
Server: &http.Server{
Addr: ":80",
Handler: r,
},
ShutdownTimeout: time.Second * 5,
}
}
// allowAllOriginsMiddleware sets appropriate CORS headers to allow requests from any origin.
func allowAllOriginsMiddleware(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin")
if origin == "" {
origin = "*"
}
w.Header().Set("Access-Control-Allow-Origin", origin)
h.ServeHTTP(w, r)
})
}
func writeJSON(rw http.ResponseWriter, value interface{}, status int) {
bytes, err := json.Marshal(value)
if err != nil {
@ -57,20 +22,12 @@ func writeJSON(rw http.ResponseWriter, value interface{}, status int) {
}
}
func writeImage(rw http.ResponseWriter, data []byte, status int, imgType string) error {
switch imgType {
case "png":
rw.Header().Set("Content-Type", "image/png")
case "svg":
rw.Header().Set("Content-Type", "image/svg+xml")
default:
return fmt.Errorf("unhandled image type: %s", imgType)
}
func writeSVG(rw http.ResponseWriter, data []byte, status int) {
rw.Header().Set("Content-Type", "image/svg+xml")
rw.WriteHeader(status)
if _, err := rw.Write(data); err != nil {
return fmt.Errorf("could not write image bytes: %w", err)
panic("could not write bytes to response: " + err.Error())
}
return nil
}
func writeErr(rw http.ResponseWriter, err error, status int) {
@ -84,72 +41,59 @@ func writeErr(rw http.ResponseWriter, err error, status int) {
// URLParam is the URL parameter getDiagramFromGET uses to look for data.
const URLParam = "data"
func getDiagramFromGET(r *http.Request, imgType string) (*Diagram, error) {
func getDiagramFromGET(rw http.ResponseWriter, r *http.Request) *Diagram {
if r.Method != http.MethodGet {
return nil, fmt.Errorf("expected HTTP method GET")
writeErr(rw, fmt.Errorf("expected HTTP method GET"), http.StatusBadRequest)
return nil
}
queryVal := strings.TrimSpace(r.URL.Query().Get(URLParam))
if queryVal == "" {
return nil, fmt.Errorf("missing data")
writeErr(rw, fmt.Errorf("missing data"), http.StatusBadRequest)
return nil
}
data, err := url.QueryUnescape(queryVal)
if err != nil {
return nil, fmt.Errorf("could not read query param: %s", err)
writeErr(rw, fmt.Errorf("could not read query param: %s", err), http.StatusBadRequest)
return nil
}
// Create a diagram from the description
d := NewDiagram([]byte(data), imgType)
return d, nil
d := NewDiagram([]byte(data))
return d
}
func getDiagramFromPOST(r *http.Request, imgType string) (*Diagram, error) {
func getDiagramFromPOST(rw http.ResponseWriter, r *http.Request) *Diagram {
if r.Method != http.MethodPost {
return nil, fmt.Errorf("expected HTTP method POST")
writeErr(rw, fmt.Errorf("expected HTTP method POST"), http.StatusBadRequest)
return nil
}
// Get description from request body
bytes, err := ioutil.ReadAll(r.Body)
if err != nil {
return nil, fmt.Errorf("could not read body: %s", err)
writeErr(rw, fmt.Errorf("could not read body: %s", err), http.StatusInternalServerError)
return nil
}
// Create a diagram from the description
d := NewDiagram(bytes, imgType)
return d, nil
d := NewDiagram(bytes)
return d
}
const URLParamImageType = "type"
// generateHTTPHandler returns a HTTP handler used to generate a diagram.
func generateHTTPHandler(generator Generator) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
func generateHTTPHandler(generator Generator) func(rw http.ResponseWriter, r *http.Request) {
return func(rw http.ResponseWriter, r *http.Request) {
var diagram *Diagram
imgType := r.URL.Query().Get(URLParamImageType)
switch imgType {
case "png", "svg":
case "":
imgType = "svg"
default:
writeErr(rw, fmt.Errorf("unsupported image type (%s) use svg or png", imgType), http.StatusBadRequest)
return
}
var err error
switch r.Method {
case http.MethodGet:
diagram, err = getDiagramFromGET(r, imgType)
diagram = getDiagramFromGET(rw, r)
case http.MethodPost:
diagram, err = getDiagramFromPOST(r, imgType)
diagram = getDiagramFromPOST(rw, r)
default:
writeErr(rw, fmt.Errorf("unexpected HTTP method %s", r.Method), http.StatusBadRequest)
return
}
if err != nil {
writeErr(rw, err, http.StatusBadRequest)
return
}
if diagram == nil {
writeErr(rw, fmt.Errorf("could not create diagram"), http.StatusInternalServerError)
return
@ -168,8 +112,6 @@ func generateHTTPHandler(generator Generator) http.Handler {
writeErr(rw, fmt.Errorf("could not read diagram bytes: %s", err), http.StatusInternalServerError)
return
}
if err := writeImage(rw, diagramBytes, http.StatusOK, imgType); err != nil {
writeErr(rw, fmt.Errorf("could not write diagram: %w", err), http.StatusInternalServerError)
}
})
writeSVG(rw, diagramBytes, http.StatusOK)
}
}

47
internal/http_service.go Normal file
View File

@ -0,0 +1,47 @@
package internal
import (
"net/http"
)
// NewHTTPService returns a service that can be used to start a http server
// that will generate diagrams.
func NewHTTPService(generator Generator) *httpService {
return &httpService{
generator: generator,
}
}
// httpService is a service that can be used to start a http server
// that will generate diagrams.
type httpService struct {
httpServer *http.Server
generator Generator
}
// Start starts the HTTP server.
func (s *httpService) Start() error {
httpHandler := generateHTTPHandler(s.generator)
r := http.NewServeMux()
r.Handle("/generate", http.HandlerFunc(httpHandler))
s.httpServer = &http.Server{
Addr: ":80",
Handler: r,
}
if err := s.httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
if err != http.ErrServerClosed {
return err
}
}
return nil
}
func (s *httpService) Stop() {
if s != nil {
_ = s.httpServer.Close()
}
}

File diff suppressed because it is too large Load Diff

View File

@ -9,6 +9,6 @@
"author": "",
"license": "ISC",
"dependencies": {
"@mermaid-js/mermaid-cli": "^11.4.2"
"@mermaid-js/mermaid-cli": "^8.6.4"
}
}

View File

@ -1,69 +0,0 @@
# Quadrant Chart Examples (Fixed)
These examples should work correctly with the updated conversion script that treats quadrant charts as convertible types:
## 1. Simple Business Priority Matrix
```
quadrantChart
title Business Priorities
x-axis Low Effort --> High Effort
y-axis Low Impact --> High Impact
quadrant-1 Do First
quadrant-2 Schedule
quadrant-3 Delegate
quadrant-4 Eliminate
Task A: [0.3, 0.9]
Task B: [0.7, 0.7]
Task C: [0.2, 0.3]
Task D: [0.8, 0.2]
```
## 2. Product Analysis
```
quadrantChart
title Product Analysis
x-axis Low Cost --> High Cost
y-axis Low Value --> High Value
quadrant-1 Quick Wins
quadrant-2 Strategic
quadrant-3 Fill-ins
quadrant-4 Questionable
Product A: [0.2, 0.8]
Product B: [0.8, 0.9]
Product C: [0.3, 0.2]
Product D: [0.7, 0.3]
```
## 3. Technology Assessment
```
quadrantChart
title Technology Stack
x-axis Low Complexity --> High Complexity
y-axis Low ROI --> High ROI
quadrant-1 Adopt
quadrant-2 Trial
quadrant-3 Assess
quadrant-4 Hold
React: [0.3, 0.8]
Vue: [0.2, 0.7]
Angular: [0.8, 0.6]
jQuery: [0.1, 0.2]
```
## How This Will Convert:
**Input (multiline):**
```
quadrantChart
title Business Priorities
x-axis Low Effort --> High Effort
y-axis Low Impact --> High Impact
quadrant-1 Do First
```
**Output (single line with semicolons):**
```
quadrantChart; title Business Priorities; x-axis Low Effort --> High Effort; y-axis Low Impact --> High Impact; quadrant-1 Do First
```
This should fix the text rendering issue where quadrant labels were getting concatenated!

View File

@ -1,36 +0,0 @@
// Simple POST fix - back to basics with proper headers
export const code = async (inputs) => {
const { diagram, format } = inputs;
if (!diagram || !diagram.trim()) {
throw new Error("No Mermaid input provided");
}
// Ensure proper line endings (just normalize, don't over-process)
const cleanDiagram = diagram.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
console.log("Sending diagram (length: " + cleanDiagram.length + "):");
console.log(JSON.stringify(cleanDiagram));
const url = `https://diagrams.starbit.cloud/generate?type=${format}`;
const resp = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "text/plain",
"Content-Length": cleanDiagram.length.toString()
},
body: cleanDiagram
});
if (!resp.ok) {
const txt = await resp.text();
throw new Error(`Mermaid-server error ${resp.status}: ${txt}`);
}
const buffer = Buffer.from(await resp.arrayBuffer());
const mime = format === "png" ? "image/png" : "image/svg+xml";
const file = `data:${mime};base64,${buffer.toString("base64")}`;
return { file };
};

View File

@ -1,10 +0,0 @@
graph TD
A[Website Analytics] --> B[Traffic: 1.2M visitors]
A --> C[Revenue: $450K]
A --> D[Growth: +25%]
B --> E[Desktop: 60%]
B --> F[Mobile: 40%]
C --> G[Q1: $95K]
C --> H[Q2: $115K]
C --> I[Q3: $125K]
C --> J[Q4: $115K]

View File

@ -1,50 +0,0 @@
# Single Line Mermaid Examples
## Basic Flowchart (Single Line)
```
graph TD; A[Analytics]-->B[Traffic]; A-->C[Revenue]; B-->D[Desktop]; B-->E[Mobile]
```
## Simple Analytics Dashboard
```
graph LR; A[Website]-->B[Users: 1.2M]; A-->C[Revenue: $450K]; A-->D[Growth: +25%]
```
## Process Flow
```
graph TD; Start[Start]-->Process[Process Data]-->Decision{Valid?}-->|Yes|End[Complete]; Decision-->|No|Error[Error]
```
## Network Diagram
```
graph LR; Client[Client]-->LB[Load Balancer]-->Server1[Server 1]; LB-->Server2[Server 2]; Server1-->DB[(Database)]; Server2-->DB
```
## Sequence Diagram (Single Line)
```
sequenceDiagram; participant A as User; participant B as System; A->>B: Request; B-->>A: Response
```
## Pie Chart (Single Line)
```
pie title Traffic Sources; "Direct": 40; "Search": 35; "Social": 15; "Referral": 10
```
## Git Flow
```
gitGraph; commit; commit; branch develop; commit; commit; checkout main; merge develop
```
## Simple Class Diagram
```
classDiagram; class User { +name: string +email: string +login() }; class Admin { +permissions: array +manage() }; User <|-- Admin
```
## State Diagram
```
stateDiagram-v2; [*]-->Idle; Idle-->Processing: start; Processing-->Complete: finish; Complete-->[*]
```
## Journey Map
```
journey; title User Shopping Journey; section Discovery; Search: 5: User; Compare: 3: User; section Purchase; Add to Cart: 4: User; Checkout: 2: User

View File

@ -1,52 +0,0 @@
// Smart mermaid conversion script that converts multiline to single-line format
export const code = async (inputs) => {
const raw = String(inputs.diagram || "").trim();
if (!raw) throw new Error("No Mermaid input provided");
// Normalize line endings first
let normalizedDiagram = raw
.replace(/\r\n/g, '\n') // Convert Windows line endings
.replace(/\r/g, '\n'); // Convert old Mac line endings
console.log("Original diagram:");
console.log(normalizedDiagram);
// Check if it's already single line
if (!normalizedDiagram.includes('\n')) {
console.log("Already single line, returning as-is");
return { rawDsl: normalizedDiagram };
}
// Convert multiline to single line with semicolons
// This works for: graph, flowchart, sequenceDiagram, pie, gitGraph
const convertibleTypes = ['graph', 'flowchart', 'sequenceDiagram', 'pie', 'gitGraph'];
const firstWord = normalizedDiagram.split(/\s+/)[0];
if (convertibleTypes.some(type => firstWord.startsWith(type))) {
// Convert newlines to semicolons, preserving the first line structure
const lines = normalizedDiagram.split('\n');
const firstLine = lines[0];
const remainingLines = lines.slice(1)
.filter(line => line.trim()) // Remove empty lines
.map(line => line.trim()); // Remove indentation
const singleLine = firstLine + '; ' + remainingLines.join('; ');
console.log("Converted to single line:");
console.log(singleLine);
return { rawDsl: singleLine };
}
// For non-convertible types (journey, stateDiagram, classDiagram),
// try replacing newlines with specific delimiters that might work
console.log("Non-convertible type detected, using special encoding");
// Try using a special marker that we can detect and replace on server side
const encodedDiagram = normalizedDiagram.replace(/\n/g, ' NEWLINE ');
console.log("Encoded diagram:");
console.log(encodedDiagram);
return { rawDsl: encodedDiagram };
};

View File

@ -1,10 +0,0 @@
%%{init: {"theme": "base", "themeVariables": {"primaryColor": "#ff6b6b", "primaryTextColor": "#fff", "primaryBorderColor": "#ff4757", "lineColor": "#5f27cd", "secondaryColor": "#00d2d3", "tertiaryColor": "#ff9ff3"}}}%%
xychart-beta
title "Monthly Website Performance Metrics - 2024"
x-axis [Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec]
y-axis "Metrics Count" 0 --> 120000
bar "Unique Visitors" [45000, 52000, 48000, 67000, 72000, 85000, 91000, 88000, 95000, 102000, 108000, 115000]
bar "Page Views" [89000, 104000, 96000, 134000, 144000, 170000, 182000, 176000, 190000, 204000, 216000, 230000]
bar "Sessions" [38000, 44000, 41000, 58000, 62000, 73000, 78000, 75000, 82000, 88000, 93000, 99000]
bar "Bounce Rate %" [45, 42, 48, 38, 35, 32, 30, 33, 29, 27, 25, 23]

View File

@ -1,47 +0,0 @@
// Test different mermaid syntaxes to see which ones work
const testDiagrams = [
{
name: "Minimal flowchart",
diagram: "flowchart TD\nA-->B"
},
{
name: "Graph syntax",
diagram: "graph TD\nA-->B"
},
{
name: "Sequence diagram",
diagram: "sequenceDiagram\nAlice->>Bob: Hello"
},
{
name: "Pie chart",
diagram: 'pie title NETFLIX\n"Time spent looking for movie" : 90\n"Time spent watching it" : 10'
},
{
name: "Single line flowchart",
diagram: "graph LR; A-->B-->C"
},
{
name: "Semicolon separated",
diagram: "graph TD; A[Start]-->B[End]"
}
];
testDiagrams.forEach((test, i) => {
console.log(`\n=== Test ${i+1}: ${test.name} ===`);
console.log("Raw:", JSON.stringify(test.diagram));
console.log("Display:");
console.log(test.diagram);
console.log("Has newlines:", test.diagram.includes('\n'));
});
// Test if semicolons can replace newlines
console.log("\n=== Semicolon replacement test ===");
const original = `graph TD
A[Analytics] --> B[Traffic]
A --> C[Revenue]`;
const semicolonVersion = original.replace(/\n\s*/g, '; ');
console.log("Original:", JSON.stringify(original));
console.log("Semicolon version:", JSON.stringify(semicolonVersion));
console.log("Display semicolon version:");
console.log(semicolonVersion);

View File

@ -1,30 +0,0 @@
// Test URL encoding to see if this preserves newlines properly
const diagram = `graph TD
A[Website Analytics] --> B[Traffic]
A --> C[Revenue]`;
console.log("Original diagram:");
console.log(JSON.stringify(diagram));
console.log("\nURL encoded:");
const encoded = encodeURIComponent(diagram);
console.log(encoded);
console.log("\nDecoded back:");
const decoded = decodeURIComponent(encoded);
console.log(JSON.stringify(decoded));
console.log("\nAre they equal?", diagram === decoded);
// Show what the healthcheck URL looks like
const healthcheckDiagram = "graph TD\n A[Test]";
const healthcheckEncoded = encodeURIComponent(healthcheckDiagram);
console.log("\nHealthcheck diagram encoded:");
console.log(healthcheckEncoded);
// Test the exact healthcheck URL from docker-compose
const dockerHealthcheck = "graph%20TD%0A%20%20%20%20A%5BTest%5D";
console.log("\nDocker healthcheck URL param:");
console.log(dockerHealthcheck);
console.log("Decoded:");
console.log(decodeURIComponent(dockerHealthcheck));

View File

@ -1,35 +0,0 @@
// Test URLSearchParams encoding vs manual encoding
const diagram = `graph TD
A[Website Analytics] --> B[Traffic]
A --> C[Revenue]`;
console.log("=== Manual encodeURIComponent ===");
const manualEncoded = encodeURIComponent(diagram);
console.log("Manual:", manualEncoded);
console.log("\n=== URLSearchParams ===");
const params = new URLSearchParams();
params.set('data', diagram);
params.set('type', 'svg');
console.log("URLSearchParams:", params.toString());
console.log("\n=== Decoded comparison ===");
const manualDecoded = decodeURIComponent(manualEncoded);
const paramsDecoded = decodeURIComponent(params.get('data'));
console.log("Manual decoded:", JSON.stringify(manualDecoded));
console.log("Params decoded:", JSON.stringify(paramsDecoded));
console.log("Are equal:", manualDecoded === paramsDecoded);
// Test with problematic characters
console.log("\n=== Testing problematic chars ===");
const problematic = `graph TD
A[Test] --> B[End]`;
const problemParams = new URLSearchParams();
problemParams.set('data', problematic);
console.log("Problematic encoded:", problemParams.toString());
// Show exact URL that would be generated
console.log("\n=== Full URL ===");
const fullUrl = `https://diagrams.starbit.cloud/generate?${params.toString()}`;
console.log("URL length:", fullUrl.length);
console.log("URL:", fullUrl);

View File

@ -1,42 +0,0 @@
// Test script that mimics your workflow exactly
const diagram = `graph TD
A[Website Analytics] --> B[Traffic]
A --> C[Revenue]
B --> D[Desktop]
B --> E[Mobile]`;
console.log("Raw diagram string:");
console.log(JSON.stringify(diagram));
console.log("\nDiagram content:");
console.log(diagram);
console.log("\nLength:", diagram.length);
// Test the conversion step
const convertMermaidBlock = (inputs) => {
const raw = String(inputs.diagram || "").trim();
if (!raw) throw new Error("No Mermaid input provided");
return { rawDsl: raw };
};
// Test the API call step
const makeApiCall = async (inputs) => {
const { diagram, format } = inputs;
if (!diagram || !diagram.trim()) {
throw new Error("No Mermaid input provided");
}
console.log("Sending to API:");
console.log("URL: https://diagrams.starbit.cloud/generate?type=" + encodeURIComponent(format));
console.log("Content-Type: text/plain");
console.log("Body:", JSON.stringify(diagram));
return { success: true };
};
// Run the test
const result1 = convertMermaidBlock({ diagram });
console.log("\nStep 1 result:", result1);
const result2 = makeApiCall({ diagram: result1.rawDsl, format: "svg" });
console.log("\nStep 2 result:", result2);

View File

@ -1,52 +0,0 @@
// Updated smart mermaid conversion script with quadrantChart fix
export const code = async (inputs) => {
const raw = String(inputs.diagram || "").trim();
if (!raw) throw new Error("No Mermaid input provided");
// Normalize line endings first
let normalizedDiagram = raw
.replace(/\r\n/g, '\n') // Convert Windows line endings
.replace(/\r/g, '\n'); // Convert old Mac line endings
console.log("Original diagram:");
console.log(normalizedDiagram);
// Check if it's already single line
if (!normalizedDiagram.includes('\n')) {
console.log("Already single line, returning as-is");
return { rawDsl: normalizedDiagram };
}
// Convert multiline to single line with semicolons
// Added quadrantChart to convertible types to fix the text rendering issue
const convertibleTypes = ['graph', 'flowchart', 'sequenceDiagram', 'pie', 'gitGraph', 'quadrantChart'];
const firstWord = normalizedDiagram.split(/\s+/)[0];
if (convertibleTypes.some(type => firstWord.startsWith(type))) {
// Convert newlines to semicolons, preserving the first line structure
const lines = normalizedDiagram.split('\n');
const firstLine = lines[0];
const remainingLines = lines.slice(1)
.filter(line => line.trim()) // Remove empty lines
.map(line => line.trim()); // Remove indentation
const singleLine = firstLine + '; ' + remainingLines.join('; ');
console.log("Converted to single line:");
console.log(singleLine);
return { rawDsl: singleLine };
}
// For non-convertible types (journey, stateDiagram, classDiagram),
// try replacing newlines with specific delimiters that might work
console.log("Non-convertible type detected, using special encoding");
// Try using a special marker that we can detect and replace on server side
const encodedDiagram = normalizedDiagram.replace(/\n/g, ' NEWLINE ');
console.log("Encoded diagram:");
console.log(encodedDiagram);
return { rawDsl: encodedDiagram };
};

View File

@ -1,23 +0,0 @@
%%{init: {"theme": "base", "themeVariables": {"primaryColor": "#1f77b4", "primaryTextColor": "#fff", "primaryBorderColor": "#ff7f0e", "lineColor": "#2ca02c"}}}%%
flowchart TD
A["📊 Website Analytics Dashboard<br/>2024 Performance"] --> B["👥 Traffic Metrics"]
A --> C["📈 Growth Analysis"]
A --> D["🎯 Conversion Data"]
B --> E["Unique Visitors: 1.2M<br/>↗️ +25% from 2023"]
B --> F["Page Views: 2.8M<br/>↗️ +32% from 2023"]
B --> G["Sessions: 950K<br/>↗️ +18% from 2023"]
C --> H["Q1: 285K visitors<br/>Growth: +15%"]
C --> I["Q2: 312K visitors<br/>Growth: +22%"]
C --> J["Q3: 335K visitors<br/>Growth: +28%"]
C --> K["Q4: 268K visitors<br/>Growth: +35%"]
D --> L["Conversion Rate: 3.2%<br/>↗️ +0.8% improvement"]
D --> M["Revenue: $450K<br/>↗️ +42% increase"]
D --> N["AOV: $125<br/>↗️ +12% increase"]
style A fill:#e1f5fe,stroke:#01579b,stroke-width:3px
style B fill:#f3e5f5,stroke:#4a148c,stroke-width:2px
style C fill:#e8f5e8,stroke:#1b5e20,stroke-width:2px
style D fill:#fff3e0,stroke:#e65100,stroke-width:2px

View File

@ -1,8 +0,0 @@
%%{init: {"theme": "base", "themeVariables": {"primaryColor": "#ff6b6b", "primaryTextColor": "#fff", "primaryBorderColor": "#ff4757"}}}%%
gitGraph
commit id: "Jan: 45K visitors"
commit id: "Feb: 52K visitors"
commit id: "Mar: 48K visitors"
commit id: "Apr: 67K visitors"
commit id: "May: 72K visitors"
commit id: "Jun: 85K visitors"

View File

@ -1,66 +0,0 @@
# ✅ Working Single Line Mermaid Examples
These are **confirmed working** single-line mermaid diagrams that bypass the newline issue:
## 📊 Analytics Dashboard
```
graph LR; A[Website]-->B[Users: 1.2M]; A-->C[Revenue: $450K]; A-->D[Growth: +25%]
```
## 🔄 Basic Flowchart
```
graph TD; A[Analytics]-->B[Traffic]; A-->C[Revenue]; B-->D[Desktop]; B-->E[Mobile]
```
## ⚡ Process Flow
```
graph TD; Start[Start]-->Process[Process Data]-->Decision{Valid?}-->|Yes|End[Complete]; Decision-->|No|Error[Error]
```
## 🏗️ Network Architecture
```
graph LR; Client[Client]-->LB[Load Balancer]-->Server1[Server 1]; LB-->Server2[Server 2]; Server1-->DB[(Database)]; Server2-->DB
```
## 💬 API Sequence
```
sequenceDiagram; participant A as User; participant B as System; A->>B: Request; B-->>A: Response
```
## 📈 Traffic Sources (Pie Chart)
```
pie title Traffic Sources; "Direct": 40; "Search": 35; "Social": 15; "Referral": 10
```
## 🔀 Git Flow
```
gitGraph; commit; commit; branch develop; commit; commit; checkout main; merge develop
```
## 🎯 **Test These First:**
**Ultra Simple Test:**
```
graph TD; A[Test]-->B[Success]
```
**Basic Analytics:**
```
graph LR; A[Dashboard]-->B[Users]; A-->C[Sales]; A-->D[Traffic]
```
---
## ❌ **Don't Use These (They Require Newlines):**
- Journey diagrams
- State diagrams
- Class diagrams
- Complex entity relationship diagrams
---
## 💡 **Pro Tips:**
1. **Always use semicolons (`;`)** to separate statements
2. **Keep everything on one line**
3. **Test simple versions first** before adding complexity
4. **Flowcharts and sequence diagrams work best** in single-line format