Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@mlightcad/mtext-parser",
"version": "1.4.0",
"version": "1.4.1",
"description": "AutoCAD MText parser written in TypeScript",
"type": "module",
"main": "dist/parser.js",
Expand Down
31 changes: 31 additions & 0 deletions src/parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1863,4 +1863,35 @@ describe('MTextColor', () => {
expect(color.rgb).toEqual([1, 2, 3]);
expect(copy.rgb).toEqual([4, 5, 6]);
});

it('fromCssColor parses hex and rgb()', () => {
const hex = MTextColor.fromCssColor('#ff0000');
expect(hex).not.toBeNull();
expect(hex!.rgb).toEqual([255, 0, 0]);

const shortHex = MTextColor.fromCssColor('#0f0');
expect(shortHex).not.toBeNull();
expect(shortHex!.rgb).toEqual([0, 255, 0]);

const rgb = MTextColor.fromCssColor('rgb(0, 128, 255)');
expect(rgb).not.toBeNull();
expect(rgb!.rgb).toEqual([0, 128, 255]);
});

it('fromCssColor handles rgba() and rejects transparent', () => {
const rgba = MTextColor.fromCssColor('rgba(10, 20, 30, 0.5)');
expect(rgba).not.toBeNull();
expect(rgba!.rgb).toEqual([10, 20, 30]);

const transparent = MTextColor.fromCssColor('transparent');
expect(transparent).toBeNull();
});

it('toCssColor returns hex for RGB and null for ACI', () => {
const rgb = new MTextColor([255, 0, 0]);
expect(rgb.toCssColor()).toBe('#ff0000');

const aci = new MTextColor(1);
expect(aci.toCssColor()).toBeNull();
});
});
92 changes: 92 additions & 0 deletions src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,75 @@ export function int2rgb(value: number): RGB {
return [r, g, b];
}

function clampColorChannel(value: number): number {
return Math.max(0, Math.min(255, Math.round(value)));
}

function normalizeColorNumber(color: number): number {
return Math.max(0, Math.min(0xffffff, Math.round(color)));
}

function colorNumberToHex(color: number | null): string | null {
if (color === null) return null;
return `#${normalizeColorNumber(color).toString(16).padStart(6, '0')}`;
}

function normalizeHexColor(value: string | null | undefined): string | null {
if (!value) return null;
const normalized = value.trim().toLowerCase();
if (/^#[0-9a-f]{6}$/.test(normalized)) return normalized;
if (/^[0-9a-f]{6}$/.test(normalized)) return `#${normalized}`;
if (/^#[0-9a-f]{3}$/.test(normalized)) {
const r = normalized[1];
const g = normalized[2];
const b = normalized[3];
return `#${r}${r}${g}${g}${b}${b}`;
}
if (/^[0-9a-f]{3}$/.test(normalized)) {
const r = normalized[0];
const g = normalized[1];
const b = normalized[2];
return `#${r}${r}${g}${g}${b}${b}`;
}
return null;
}

function cssColorToRgbValue(value: string | null | undefined): number | null {
if (!value) return null;
const raw = value.trim().toLowerCase();
if (raw === 'transparent') return null;

const hex = normalizeHexColor(raw);
if (hex) {
return normalizeColorNumber(Number.parseInt(hex.slice(1), 16));
}

const fnMatch = raw.match(/^rgba?\((.*)\)$/);
if (!fnMatch) return null;

const parts = fnMatch[1]
.replace(/\s*\/\s*/g, ' ')
.split(/[,\s]+/)
.map(p => p.trim())
.filter(Boolean);

if (parts.length < 3) return null;

const toChannel = (token: string): number => {
if (token.endsWith('%')) {
const percent = Number.parseFloat(token.slice(0, -1));
return clampColorChannel((percent / 100) * 255);
}
const num = Number.parseFloat(token);
return clampColorChannel(num);
};

const r = toChannel(parts[0]);
const g = toChannel(parts[1]);
const b = toChannel(parts[2]);
return rgb2int([r, g, b]);
}

/**
* Escape DXF line endings
* @param text - Text to escape
Expand Down Expand Up @@ -1642,6 +1711,29 @@ export class MTextColor {
return { aci: this._aci, rgb: this.rgb, rgbValue: this._rgbValue };
}

/**
* Convert the current color to a CSS hex color string (#rrggbb).
* Returns null if the color is ACI-based and has no RGB value.
*/
toCssColor(): string | null {
if (this._rgbValue !== null) {
return colorNumberToHex(this._rgbValue);
}
return null;
}

/**
* Create an MTextColor from a CSS color string.
* Supports #rgb, #rrggbb, rgb(...), rgba(...). Returns null if invalid or transparent.
*/
static fromCssColor(value: string | null | undefined): MTextColor | null {
const rgbValue = cssColorToRgbValue(value);
if (rgbValue === null) return null;
const color = new MTextColor();
color.rgbValue = rgbValue;
return color;
}

/**
* Equality check for color.
* @param other The other MTextColor to compare.
Expand Down
Loading