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 src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { lstsvToDataCsv, lstsvToDdiXml } from './pipelines/lstsv2ddi/index.js';
import { lstsvToXlsform } from './pipelines/lstsv2xlsform/index.js';
import type { Submission } from './pipelines/xlsform2ddi/index.js';
import { XLSFormToTSVConverter } from './pipelines/xlsform2lstsv/index.js';
import type { XLSFormData } from './config/types.js';
import type { XLSFormData } from './xlsform/types.js';
import { XLSValidator } from './xlsform/validate.js';
import type { SubsetTarget } from './xlsform/validate.js';
import {
Expand Down
2 changes: 1 addition & 1 deletion src/cliShared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import { readFileSync, writeFileSync } from 'node:fs';
import { parseArgs, ParseArgsConfig } from 'node:util';

import { XLSFormData } from './config/types.js';
import { XLSFormData } from './xlsform/types.js';
import type { BuildDdiOptions } from './ddi/codebook.js';
import {
buildDataCsv,
Expand Down
62 changes: 16 additions & 46 deletions src/config/ConfigManager.ts
Original file line number Diff line number Diff line change
@@ -1,64 +1,34 @@
import { deepMerge } from '../utils/helpers.js';

import { ConversionConfig, defaultConfig } from './types.js';
import { resolveConfig } from './resolveConfig.js';
import { LstsvConfig } from './types.js';

export { ConversionConfig } from './types.js';

/**
* @deprecated Use {@link resolveConfig}. Kept as a thin wrapper so existing
* consumers keep working; it will go with the public-API cleanup.
*/
export class ConfigManager {
private config: ConversionConfig;

constructor(config?: Partial<ConversionConfig>) {
this.config = this.mergeConfig(config || {});
}
private config: Readonly<LstsvConfig>;

private mergeConfig(
partialConfig: Partial<ConversionConfig>,
): ConversionConfig {
return deepMerge(structuredClone(defaultConfig), partialConfig);
constructor(config?: Partial<LstsvConfig>) {
this.config = resolveConfig(config);
}

getConfig(): ConversionConfig {
getConfig(): Readonly<LstsvConfig> {
return this.config;
}

getDefaults(): ConversionConfig['defaults'] {
getDefaults(): LstsvConfig['defaults'] {
return this.config.defaults;
}

getAdvancedOptions() {
return {
autoCreateGroups: true, // Always auto-create groups (hardcoded)
handleRepeats: this.config.handleRepeats ?? 'warn',
debugLogging: this.config.debugLogging ?? false,
};
/** Replaces the options: `partialConfig` merged over the defaults (as before). */
updateConfig(partialConfig: Partial<LstsvConfig>): void {
this.config = resolveConfig(partialConfig);
}

/**
* Update configuration at runtime
*/
updateConfig(partialConfig: Partial<ConversionConfig>): void {
this.config = this.mergeConfig(partialConfig);
}

/**
* Validate configuration
*/
/** Validation now happens in {@link resolveConfig}; kept for compatibility. */
validateConfig(): void {
const { defaults } = this.config;

// Validate handleRepeats if provided
if (
this.config.handleRepeats &&
!['warn', 'error', 'ignore'].includes(this.config.handleRepeats)
) {
throw new Error(
`Invalid handleRepeats option: ${this.config.handleRepeats}`,
);
}

// Validate defaults
if (!defaults.language || defaults.language.length !== 2) {
throw new Error('defaults.language must be a 2-character language code');
}
resolveConfig(this.config);
}
}
27 changes: 27 additions & 0 deletions src/config/resolveConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { deepMerge } from '../utils/helpers.js';

import { defaultConfig, LstsvConfig } from './types.js';

const REPEAT_MODES = ['warn', 'error', 'ignore'];

/**
* Merge a partial config over the defaults and validate it, in one step. The
* result is frozen: a conversion reads its options, it never changes them.
*/
export function resolveConfig(
partial: Partial<LstsvConfig> = {},
): Readonly<LstsvConfig> {
const config = deepMerge(structuredClone(defaultConfig), partial);

if (config.handleRepeats && !REPEAT_MODES.includes(config.handleRepeats)) {
throw new Error(`Invalid handleRepeats option: ${config.handleRepeats}`);
}
// The whole xlsform2lstsv pipeline handles 2-letter codes only
// (isValidLanguageCode), although convention:languageTagging allows BCP 47
// tags such as fr-BE; tracked separately.
if (!config.defaults.language || config.defaults.language.length !== 2) {
throw new Error('defaults.language must be a 2-character language code');
}

return Object.freeze(config);
}
75 changes: 19 additions & 56 deletions src/config/types.ts
Original file line number Diff line number Diff line change
@@ -1,65 +1,25 @@
/**
* Represents a row in the survey section of an XLSForm
* Configuration for the XLSForm → LimeSurvey TSV conversion.
*
* The XLSForm row types used to live here; they are in `src/xlsform/types.ts`
* now and re-exported below for compatibility.
*/
export interface SurveyRow {
type?: string;
name?: string;
label?: string | Record<string, string>;
hint?: string | Record<string, string>;
required?: string;
relevant?: string;
constraint?: string;
constraint_message?: string;
calculation?: string;
default?: string;
_languages?: string[];
[key: string]: unknown;
}

/**
* Represents a row in the choices section of an XLSForm
*/
export interface ChoiceRow {
list_name?: string;
name?: string;
label?: string | Record<string, string>;
filter?: string;
_languages?: string[];
[key: string]: unknown;
}

/**
* Represents a row in the settings section of an XLSForm
*/
export interface SettingsRow {
form_title?: string;
form_id?: string;
default_language?: string;
style?: string;
[key: string]: unknown;
}
export type {
SurveyRow,
ChoiceRow,
SettingsRow,
XLSFormData,
} from '../xlsform/types.js';

/**
* Result type returned by XLS/XLSX loaders
* Options for the XLSForm → LimeSurvey TSV conversion (`xlsform2lstsv`). The
* other directions take their own option objects.
*/
export interface XLSFormData {
surveyData: SurveyRow[];
choicesData: ChoiceRow[];
settingsData: SettingsRow[];
hasSurveySheet: boolean;
hasChoicesSheet: boolean;
hasSettingsSheet: boolean;
}

export interface ConversionConfig {
/**
* How to handle repeats: 'warn', 'error', or 'ignore' (default: 'warn')
*/
export interface LstsvConfig {
/** @deprecated Never read; accepted so existing callers keep compiling. */
handleRepeats?: 'warn' | 'error' | 'ignore';

/**
* Enable debug logging (default: false)
*/
/** @deprecated Never read; accepted so existing callers keep compiling. */
debugLogging?: boolean;

/**
Expand Down Expand Up @@ -110,7 +70,7 @@ export interface ConversionConfig {
/**
* Default configuration with sensible defaults
*/
export const defaultConfig: ConversionConfig = {
export const defaultConfig: LstsvConfig = {
handleRepeats: 'warn',
debugLogging: false,
convertWelcomeNote: true,
Expand All @@ -127,3 +87,6 @@ export const defaultConfig: ConversionConfig = {
description: '',
},
};

/** @deprecated Use {@link LstsvConfig}. */
export type ConversionConfig = LstsvConfig;
2 changes: 1 addition & 1 deletion src/fileChoices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import * as fs from 'node:fs';
import * as path from 'node:path';

import type { ChoiceRow, SurveyRow } from './config/types.js';
import type { ChoiceRow, SurveyRow } from './xlsform/types.js';
import { parseVocabCsv, referencedVocabFiles } from './vocab.js';

/**
Expand Down
10 changes: 3 additions & 7 deletions src/generateFixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,8 @@ import * as fs from 'fs';
import * as path from 'path';
import { fileURLToPath } from 'url';

import {
SurveyRow,
ChoiceRow,
SettingsRow,
ConversionConfig,
} from './config/types.js';
import { LstsvConfig } from './config/types.js';
import { SurveyRow, ChoiceRow, SettingsRow } from './xlsform/types.js';
import { XLSLoader } from './xlsform/loader.js';
import { XLSFormToTSVConverter } from './pipelines/xlsform2lstsv/index.js';

Expand Down Expand Up @@ -58,7 +54,7 @@ function cleanOutputDirectory(dir: string): void {
async function generateTSVFromFixture(
fixturePath: string,
outputPath: string,
config: Partial<ConversionConfig> = {},
config: Partial<LstsvConfig> = {},
): Promise<void> {
console.log(`Processing: ${path.basename(fixturePath)}`);

Expand Down
8 changes: 8 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,11 @@ export type {
// ── Config ─────────────────────────────────────────────────────────────
export { ConfigManager, ConversionConfig } from './config/ConfigManager.js';
export { defaultConfig } from './config/types.js';
export type { LstsvConfig } from './config/types.js';
export { resolveConfig } from './config/resolveConfig.js';
export type {
SurveyRow,
ChoiceRow,
SettingsRow,
XLSFormData,
} from './xlsform/types.js';
2 changes: 1 addition & 1 deletion src/pipelines/lstsv2xlsform/toXlsform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
*/

import { defaultConfig } from '../../config/types.js';
import type { SurveyRow, ChoiceRow, SettingsRow } from '../../config/types.js';
import type { SurveyRow, ChoiceRow, SettingsRow } from '../../xlsform/types.js';
import { APPEARANCES } from '../../generated/Appearances.js';
import { TYPE_MAPPINGS } from '../../generated/TypeMappings.js';
import { EXCLUSIVE_RULE } from '../../xlsform/exclusive.js';
Expand Down
2 changes: 1 addition & 1 deletion src/pipelines/xlsform2lstsv/choiceManager.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ChoiceRow, SurveyRow } from '../../config/types.js';
import { ChoiceRow, SurveyRow } from '../../xlsform/types.js';
import { normalizeName } from '../../xlsform/identifiers.js';
import { FieldSanitizer } from '../../xlsform/sanitize.js';
import { TypeInfo } from './typeMapper.js';
Expand Down
2 changes: 1 addition & 1 deletion src/pipelines/xlsform2lstsv/fieldNameHandler.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SurveyRow } from '../../config/types.js';
import { SurveyRow } from '../../xlsform/types.js';
import { normalizeName } from '../../xlsform/identifiers.js';
import { FieldSanitizer } from '../../xlsform/sanitize.js';

Expand Down
2 changes: 1 addition & 1 deletion src/pipelines/xlsform2lstsv/groupEmitter.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SurveyRow } from '../../config/types.js';
import { SurveyRow } from '../../xlsform/types.js';
import { ConfigManager } from '../../config/ConfigManager.js';
import { RowEmitter } from './rowEmitter.js';
import { LanguageHandler } from './languageHandler.js';
Expand Down
2 changes: 1 addition & 1 deletion src/pipelines/xlsform2lstsv/groupProcessor.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SurveyRow } from '../../config/types.js';
import { SurveyRow } from '../../xlsform/types.js';
import { ConfigManager } from '../../config/ConfigManager.js';
import { SKIP_TYPES } from './constants.js';

Expand Down
17 changes: 8 additions & 9 deletions src/pipelines/xlsform2lstsv/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ConfigManager, ConversionConfig } from '../../config/ConfigManager.js';
import { SurveyRow, ChoiceRow, SettingsRow } from '../../config/types.js';
import { ConfigManager } from '../../config/ConfigManager.js';
import type { LstsvConfig } from '../../config/types.js';
import { SurveyRow, ChoiceRow, SettingsRow } from '../../xlsform/types.js';
import { FieldSanitizer } from '../../xlsform/sanitize.js';
import { TSVGenerator } from '../../lstsv/serialize.js';
import { TypeMapper, TYPE_MAPPINGS } from './typeMapper.js';
Expand Down Expand Up @@ -58,9 +59,8 @@ export class XLSFormToTSVConverter {
private surveySettingsEmitter: SurveySettingsEmitter;
private surveyDataCache: SurveyRow[] = [];

constructor(config?: Partial<ConversionConfig>) {
constructor(config?: Partial<LstsvConfig>) {
this.configManager = new ConfigManager(config);
this.configManager.validateConfig();

this.fieldSanitizer = new FieldSanitizer();
this.choiceManager = new ChoiceManager(this.fieldSanitizer);
Expand Down Expand Up @@ -135,14 +135,14 @@ export class XLSFormToTSVConverter {
/**
* Get the current configuration
*/
getConfig(): ConversionConfig {
getConfig(): Readonly<LstsvConfig> {
return this.configManager.getConfig();
}

/**
* Update configuration at runtime
*/
updateConfig(partialConfig: Partial<ConversionConfig>): void {
updateConfig(partialConfig: Partial<LstsvConfig>): void {
this.configManager.updateConfig(partialConfig);
}

Expand Down Expand Up @@ -216,9 +216,8 @@ export class XLSFormToTSVConverter {
return xfType === 'begin_group';
});

// If no groups, add a default group
const advancedOptions = this.configManager.getAdvancedOptions();
if (!hasGroups && advancedOptions.autoCreateGroups) {
// LimeSurvey needs every question in a group; add one if the form has none.
if (!hasGroups) {
this.groupEmitter.addDefaultGroup();
}

Expand Down
2 changes: 1 addition & 1 deletion src/pipelines/xlsform2lstsv/languageHandler.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SurveyRow, ChoiceRow, SettingsRow } from '../../config/types.js';
import { SurveyRow, ChoiceRow, SettingsRow } from '../../xlsform/types.js';
import { getBaseLanguage } from '../../utils/languageUtils.js';
import { markdownToHtml } from '../../utils/markdownRenderer.js';
import { ConfigManager } from '../../config/ConfigManager.js';
Expand Down
2 changes: 1 addition & 1 deletion src/pipelines/xlsform2lstsv/matrixHandler.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SurveyRow } from '../../config/types.js';
import { SurveyRow } from '../../xlsform/types.js';
import { TypeInfo } from './typeMapper.js';
import { RowEmitter } from './rowEmitter.js';
import { LanguageHandler } from './languageHandler.js';
Expand Down
2 changes: 1 addition & 1 deletion src/pipelines/xlsform2lstsv/otherPatternDetector.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SurveyRow, ChoiceRow } from '../../config/types.js';
import { SurveyRow, ChoiceRow } from '../../xlsform/types.js';
import { TypeInfo } from './typeMapper.js';
import { ChoiceManager } from './choiceManager.js';
import { LanguageHandler } from './languageHandler.js';
Expand Down
2 changes: 1 addition & 1 deletion src/pipelines/xlsform2lstsv/surveySettingsEmitter.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SurveyRow, SettingsRow } from '../../config/types.js';
import { SurveyRow, SettingsRow } from '../../xlsform/types.js';
import { RowEmitter } from './rowEmitter.js';
import { LanguageHandler } from './languageHandler.js';
import { ConfigManager } from '../../config/ConfigManager.js';
Expand Down
2 changes: 1 addition & 1 deletion src/vocab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* the caller's to read and pass through {@link parseVocabCsv}; the node-only
* `fileChoices.ts` does that from disk for the CLI.
*/
import type { ChoiceRow, SurveyRow } from './config/types.js';
import type { ChoiceRow, SurveyRow } from './xlsform/types.js';
import { VOCABULARY_OPTIONS } from './generated/VocabularyOptions.js';
import { parseCsvRecords } from './responseFile.js';

Expand Down
2 changes: 1 addition & 1 deletion src/xlsform/exclusive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* `exclude_all_others` question attribute.
*/
import conventions from '../generated/conventions.js';
import type { ChoiceRow } from '../config/types.js';
import type { ChoiceRow } from './types.js';

export const EXCLUSIVE_RULE = conventions.conventions.exclusiveChoice;

Expand Down
Loading
Loading