js-ini-parser
Version:
A simple ini parser that save comments and spaces
247 lines (244 loc) • 8.15 kB
JavaScript
;
const DEFAULT_SECTION_NAME = "";
class Line {
constructor(text, number) {
this.text = text;
this.number = number;
this.text = text;
this.number = number;
}
isComment() {
return this.text.startsWith(";");
}
isSection() {
return this.text.startsWith("[") && this.text.endsWith("]");
}
isEmptyKeyValuePair() {
if (this.text.length <= 1) {
return false;
}
if (this.isComment() || this.isSection()) {
return false;
}
const [, v] = this.text.trim().split("=");
return v === void 0 || v === "";
}
isKeyValuePair() {
if (this.isEmptyKeyValuePair()) {
return false;
}
const hasEqualSign = this.text.indexOf("=") !== -1;
if (!hasEqualSign) {
return false;
}
const startsWithEqualSign = this.text.startsWith("=");
if (startsWithEqualSign) {
return false;
}
if (this.text.trim().split("=").length === 2) {
return hasEqualSign && !startsWithEqualSign && !this.isComment() && !this.isSection();
}
if (this.isComment() && this.isSection()) {
return false;
}
return !startsWithEqualSign;
}
}
function parseIni(text, options) {
const sections = [];
let currentSection;
let currentBlock;
let globalSection;
let debug = console.debug;
if (options.debug !== true) {
debug = () => {
};
}
if (options.allowGlobalSection) {
globalSection = {
section: options.globalSectionName || DEFAULT_SECTION_NAME,
blocks: []
};
}
const lines = text.split("\n").filter((a) => a.trim() !== "");
for (let index = 0; index < lines.length; index++) {
const currentLine = lines[index];
const trimmedLine = new Line(currentLine.trim(), index);
if (trimmedLine.isSection()) {
currentSection = { section: trimmedLine.text.slice(1, -1), blocks: [] };
sections.push(currentSection);
currentBlock = void 0;
} else if (trimmedLine.isComment()) {
if (index < lines.length - 1) {
const nextLine = lines[index + 1];
const trimmedNextLine = new Line(nextLine.trim(), index + 1);
if (trimmedNextLine.isKeyValuePair()) {
debug("comment linked to a key-value pair");
debug(`l:${index} ${trimmedLine.text}`);
} else if (currentSection) {
currentSection.blocks.push({
type: "comment",
text: trimmedLine.text
});
} else if (options.allowGlobalSection && globalSection) {
globalSection.blocks.push({ type: "comment", text: trimmedLine.text });
} else {
console.error(`Error at line ${index} : comment outside of section`);
console.error(`l:${index} ${trimmedLine.text}`);
}
} else {
debug("last line of the file");
if (currentSection) {
currentSection.blocks.push({
type: "comment",
text: trimmedLine.text
});
} else if (options.allowGlobalSection && globalSection) {
globalSection.blocks.push({ type: "comment", text: trimmedLine.text });
} else {
console.error(`Error at line ${index} : comment outside of section`);
console.error(`l:${index} ${trimmedLine.text}`);
}
}
} else if (currentSection) {
const isKeyValuePair = trimmedLine.isKeyValuePair();
debug("isKeyValuePair", isKeyValuePair);
if (isKeyValuePair) {
const separatorIndex = trimmedLine.text.indexOf("=");
const key = trimmedLine.text.slice(0, separatorIndex).trim();
const value = trimmedLine.text.slice(separatorIndex + 1).trim();
currentBlock = { type: "data", key, value };
if (index === 0) ; else if (index > 0) {
const previousLine = lines[index - 1];
const trimmedPreviousLine = new Line(previousLine.trim(), index - 1);
if (trimmedPreviousLine.isComment()) {
currentBlock.comment = {
type: "comment",
text: trimmedPreviousLine.text
};
}
}
currentSection.blocks.push(currentBlock);
} else if (trimmedLine.isEmptyKeyValuePair()) {
if (options.allowEmptyValue) {
debug(
"empty key value pair at line " + index + " : " + trimmedLine.text
);
const separatorIndex = trimmedLine.text.indexOf("=");
currentBlock = {
type: "data",
key: trimmedLine.text.slice(0, separatorIndex).trim(),
value: ""
};
currentSection.blocks.push(currentBlock);
} else {
console.error(
`Empty value at ${index}. This line should have a value.`
);
console.error(`Line content: ${trimmedLine.text}`);
}
} else {
console.error(
`Invalid line ${index}. This line should be a key-value pair.`
);
console.error(`Line content: ${trimmedLine.text}`);
}
} else if (options.allowGlobalSection && globalSection) {
sections.push(globalSection);
} else {
console.warn(`Ignoring data outside of section`);
console.warn(`l:${index} ${trimmedLine.text}`);
}
}
return sections;
}
function stringifyIni(sections, options) {
const lines = [];
for (const section of sections) {
lines.push(`[${section.section}]`);
for (const block of section.blocks) {
if (block.type === "data") {
if (block.comment && block.comment.type === "comment") {
lines.push(`${block.comment.text}`);
lines.push(`${block.key}=${block.value}`);
} else {
lines.push(`${block.key}=${block.value}`);
}
} else if (block.type === "comment") {
lines.push(`${block.text}`);
}
}
}
return lines.join("\n");
}
function addSection(obj, sectionName) {
obj.push({ section: sectionName, blocks: [] });
return { section: sectionName, blocks: [] };
}
function addKeyValue(obj, section, key, value, options) {
const targetSection = obj.find((s) => s.section === section);
if (!targetSection) {
throw new Error(`Section ${section} not found`);
}
const block = targetSection.blocks.find(
(b) => b.type === "data" && b.key === key
);
if (block) {
if (options?.override) {
if (block && block.type === "data") {
block.value = value;
}
return block;
}
throw new Error(
`Key ${key} already exists in section ${section}, see options.override`
);
}
targetSection.blocks.push({ type: "data", key, value });
return { type: "data", key, value };
}
function addComment(obj, section, comment, options) {
comment = ";" + comment;
const targetSection = obj.find((s) => s.section === section);
if (!targetSection) {
throw new Error(`Section ${section} not found`);
}
if (options?.attachToKey) {
const keyPosition = targetSection.blocks.findIndex(
(b) => b.type === "data" && b.key === options.attachToKey
);
const commentPosition = keyPosition - 1;
targetSection.blocks.splice(commentPosition, 0, {
type: "comment",
text: comment
});
return targetSection.blocks[commentPosition];
}
targetSection.blocks.push({ type: "comment", text: comment });
return { type: "comment", text: comment };
}
function getSection(sections, sectionName) {
return sections.find((section) => section.section === sectionName);
}
function updateKeyValue(obj, section, key, value) {
const targetSection = obj.find((s) => s.section === section);
if (!targetSection) {
throw new Error(`Section ${section} not found`);
}
const block = targetSection.blocks.find(
(b) => b.type === "data" && b.key === key
);
if (block && block.type === "data") {
block.value = value;
return block;
}
throw new Error(`Key ${key} not found in section ${section}. Can't update.`);
}
exports.Line = Line;
exports.addComment = addComment;
exports.addKeyValue = addKeyValue;
exports.addSection = addSection;
exports.getSection = getSection;
exports.parseIni = parseIni;
exports.stringifyIni = stringifyIni;
exports.updateKeyValue = updateKeyValue;