super-productivity/src/main.ts
Johannes Millan 02959b56ca fix(markdown): restore inline formatting and links for marked v17
The upgrade to marked v17 (commit 4bdd94cbf) broke several markdown
features:
- Bold/italic text not rendering
- Link syntax [text](url) not working
- Plain URLs not becoming clickable

Root causes:
1. In marked v17, renderer methods receive token objects with a
   `tokens` array. To get rendered HTML with inline formatting,
   you must call `this.parser.parseInline(tokens)` instead of
   using the raw `text` property.

2. The redundant `provideMarkdown()` call in main.ts was overriding
   the `MarkdownModule.forRoot()` configuration that included the
   custom markedOptionsFactory.

3. The custom renderer.text URL linkifier was causing double-processing
   since marked v17 with gfm:true already auto-links URLs at the
   lexer level.

Changes:
- Convert listitem, link, paragraph renderers to regular functions
  to access `this.parser.parseInline(tokens)` for inline rendering
- Restore image renderer with sizing syntax support
- Restore preprocessing hooks for image sizing (=WIDTHxHEIGHT)
- Remove custom URL linkifier (GFM handles it natively)
- Remove redundant provideMarkdown() from main.ts

Fixes #6181
2026-01-26 13:48:10 +01:00

315 lines
12 KiB
TypeScript

import {
APP_INITIALIZER,
enableProdMode,
ErrorHandler,
importProvidersFrom,
provideZonelessChangeDetection,
SecurityContext,
} from '@angular/core';
import { registerLocaleData } from '@angular/common';
import { environment } from './environments/environment';
import { IS_ELECTRON } from './app/app.constants';
import { DEFAULT_LANGUAGE, LocalesImports } from './app/core/locale.constants';
import { IS_ANDROID_WEB_VIEW } from './app/util/is-android-web-view';
import { androidInterface } from './app/features/android/android-interface';
import { IS_IOS_NATIVE, IS_NATIVE_PLATFORM } from './app/util/is-native-platform';
// Type definitions for window.ea are in ./app/core/window-ea.d.ts
import { App as CapacitorApp } from '@capacitor/app';
import { GlobalErrorHandler } from './app/core/error-handler/global-error-handler.class';
import { bootstrapApplication, BrowserModule } from '@angular/platform-browser';
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { MarkdownModule, MARKED_OPTIONS, SANITIZE } from 'ngx-markdown';
import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field';
import { FeatureStoresModule } from './app/root-store/feature-stores.module';
import { MATERIAL_ANIMATIONS, MatNativeDateModule } from '@angular/material/core';
import { FormlyConfigModule } from './app/ui/formly-config.module';
import { markedOptionsFactory } from './app/ui/marked-options-factory';
import { MaterialCssVarsModule } from 'angular-material-css-vars';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatBottomSheetModule } from '@angular/material/bottom-sheet';
import { ReminderModule } from './app/features/reminder/reminder.module';
import { provideAnimations } from '@angular/platform-browser/animations';
import {
PreloadAllModules,
provideRouter,
withHashLocation,
withPreloading,
} from '@angular/router';
import { APP_ROUTES } from './app/app.routes';
import { StoreModule } from '@ngrx/store';
import { META_REDUCERS } from './app/root-store/meta/meta-reducer-registry';
import { setOperationCaptureService } from './app/root-store/meta/task-shared-meta-reducers';
import { OperationCaptureService } from './app/op-log/capture/operation-capture.service';
import { EncryptionPasswordDialogOpenerInitService } from './app/imex/sync/encryption-password-dialog-opener-init.service';
import { EffectsModule } from '@ngrx/effects';
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
import { ReactiveFormsModule } from '@angular/forms';
import { ServiceWorkerModule } from '@angular/service-worker';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import {
TRANSLATE_HTTP_LOADER_CONFIG,
TranslateHttpLoader,
} from '@ngx-translate/http-loader';
import { CdkDropListGroup } from '@angular/cdk/drag-drop';
import { AppComponent } from './app/app.component';
import { ShortTimeHtmlPipe } from './app/ui/pipes/short-time-html.pipe';
import { ShortTimePipe } from './app/ui/pipes/short-time.pipe';
import { BackgroundTask } from '@capawesome/capacitor-background-task';
import { promiseTimeout } from './app/util/promise-timeout';
import { PLUGIN_INITIALIZER_PROVIDER } from './app/plugins/plugin-initializer';
import { initializeMatMenuTouchFix } from './app/features/tasks/task-context-menu/mat-menu-touch-monkey-patch';
import { Log } from './app/core/log';
import { GlobalConfigService } from './app/features/config/global-config.service';
import { LocaleDatePipe } from './app/ui/pipes/locale-date.pipe';
if (environment.production || environment.stage) {
enableProdMode();
}
// Window.ea declaration is in src/app/core/window-ea.d.ts
bootstrapApplication(AppComponent, {
providers: [
// Provide configuration for TranslateHttpLoader
{
provide: TRANSLATE_HTTP_LOADER_CONFIG,
useValue: {
prefix: './assets/i18n/',
suffix: '.json',
},
},
importProvidersFrom(
FeatureStoresModule,
MatNativeDateModule,
FormlyConfigModule,
MarkdownModule.forRoot({
markedOptions: {
provide: MARKED_OPTIONS,
useFactory: markedOptionsFactory,
},
sanitize: { provide: SANITIZE, useValue: SecurityContext.HTML },
}),
MaterialCssVarsModule.forRoot(),
MatSidenavModule,
MatBottomSheetModule,
ReminderModule,
MaterialCssVarsModule.forRoot(),
// External
BrowserModule,
// NOTE: both need to be present to use forFeature stores
// Meta-reducers are defined in meta-reducer-registry.ts with detailed phase documentation
StoreModule.forRoot(undefined, {
metaReducers: META_REDUCERS,
...(environment.production
? {
runtimeChecks: {
strictStateImmutability: false,
strictActionImmutability: false,
strictStateSerializability: false,
strictActionSerializability: false,
},
}
: {
runtimeChecks: {
strictStateImmutability: true,
strictActionImmutability: true,
strictStateSerializability: true,
strictActionSerializability: true,
strictActionTypeUniqueness: true,
},
}),
}),
EffectsModule.forRoot([]),
!environment.production && !environment.stage
? StoreDevtoolsModule.instrument({
maxAge: 15,
logOnly: environment.production,
actionsBlocklist: ['[TimeTracking] Add time spent'],
})
: [],
ReactiveFormsModule,
ServiceWorkerModule.register('ngsw-worker.js', {
enabled:
!IS_ELECTRON &&
!IS_NATIVE_PLATFORM &&
(environment.production || environment.stage),
// Register the ServiceWorker as soon as the application is stable
// or after 30 seconds (whichever comes first).
registrationStrategy: 'registerWhenStable:30000',
}),
TranslateModule.forRoot({
fallbackLang: DEFAULT_LANGUAGE,
loader: {
provide: TranslateLoader,
useClass: TranslateHttpLoader,
},
}),
CdkDropListGroup,
),
{ provide: ErrorHandler, useClass: GlobalErrorHandler },
provideHttpClient(withInterceptorsFromDi()),
LocaleDatePipe,
ShortTimeHtmlPipe,
ShortTimePipe,
{
provide: MAT_FORM_FIELD_DEFAULT_OPTIONS,
useValue: { appearance: 'fill', subscriptSizing: 'dynamic' },
},
provideAnimations(),
{
provide: MATERIAL_ANIMATIONS,
deps: [GlobalConfigService],
useFactory: (globalConfigService: GlobalConfigService) => ({
get animationsDisabled(): boolean {
return globalConfigService.misc()?.isDisableAnimations ?? false;
},
}),
},
provideRouter(APP_ROUTES, withHashLocation(), withPreloading(PreloadAllModules)),
PLUGIN_INITIALIZER_PROVIDER,
provideZonelessChangeDetection(),
// Initialize operation capture service for synchronous state change capture
// This must run before any persistent actions are dispatched
{
provide: APP_INITIALIZER,
useFactory: (captureService: OperationCaptureService) => {
return () => {
setOperationCaptureService(captureService);
};
},
deps: [OperationCaptureService],
multi: true,
},
// Initialize encryption password dialog opener for static form config functions
{
provide: APP_INITIALIZER,
useFactory: (_initService: EncryptionPasswordDialogOpenerInitService) => {
// Service constructor initializes the module-level reference
return () => {};
},
deps: [EncryptionPasswordDialogOpenerInitService],
multi: true,
},
// Note: ImmediateUploadService now initializes itself in constructor
// after DataInitStateService.isAllDataLoadedInitially$ fires to avoid
// race condition where upload attempts happen before sync config is loaded
],
}).then(() => {
// Initialize touch fix for Material menus
initializeMatMenuTouchFix();
// Register default locale immediately for fast startup
registerLocaleData(LocalesImports[DEFAULT_LANGUAGE], DEFAULT_LANGUAGE);
// Defer other locales to idle time for better initial load performance
if (typeof requestIdleCallback === 'function') {
requestIdleCallback(() => {
Object.keys(LocalesImports).forEach((locale) => {
if (locale !== DEFAULT_LANGUAGE) {
registerLocaleData(LocalesImports[locale], locale);
}
});
});
} else {
// Fallback for browsers without requestIdleCallback
setTimeout(() => {
Object.keys(LocalesImports).forEach((locale) => {
if (locale !== DEFAULT_LANGUAGE) {
registerLocaleData(LocalesImports[locale], locale);
}
});
}, 0);
}
// TODO make asset caching work for electron
if (
'serviceWorker' in navigator &&
(environment.production || environment.stage) &&
!IS_ELECTRON &&
!IS_NATIVE_PLATFORM
) {
Log.log('Registering Service worker');
return navigator.serviceWorker.register('ngsw-worker.js').catch((err: unknown) => {
Log.log('Service Worker Registration Error');
Log.err(err);
});
} else if ('serviceWorker' in navigator && (IS_ELECTRON || IS_NATIVE_PLATFORM)) {
navigator.serviceWorker
.getRegistrations()
.then((registrations) => {
for (const registration of registrations) {
registration.unregister();
}
})
.catch((e) => {
Log.err('ERROR when unregistering service worker');
Log.err(e);
});
}
return undefined;
});
// fix mobile scrolling while dragging
window.addEventListener('touchmove', () => {});
if (!(environment.production || environment.stage) && IS_ANDROID_WEB_VIEW) {
setTimeout(() => {
androidInterface.showToast('Android DEV works');
Log.log(androidInterface);
}, 1000);
}
// CAPACITOR STUFF
// ---------------
// Android-specific: Handle back button
if (IS_ANDROID_WEB_VIEW) {
CapacitorApp.addListener('backButton', ({ canGoBack }) => {
if (!canGoBack) {
CapacitorApp.minimizeApp();
} else {
window.history.back();
}
});
}
// Android: Handle app state changes with background task for sync completion
if (IS_ANDROID_WEB_VIEW) {
CapacitorApp.addListener('appStateChange', async ({ isActive }) => {
if (isActive) {
return;
}
// The app state has been changed to inactive.
// Start the background task by calling `beforeExit`.
const taskId = await BackgroundTask.beforeExit(async () => {
// Run your code...
// Finish the background task as soon as everything is done.
Log.log('Time window for completing sync started');
await promiseTimeout(20000);
Log.log('Time window for completing sync ended. Closing app!');
BackgroundTask.finish({ taskId });
});
});
}
// iOS: Handle app state changes (limited background time)
if (IS_IOS_NATIVE) {
CapacitorApp.addListener('appStateChange', async ({ isActive }) => {
if (isActive) {
Log.log('iOS app became active');
return;
}
// iOS has limited background execution time (~30 seconds)
// Log state change but don't attempt long-running tasks
Log.log('iOS app going to background');
});
// Handle app URL open (for OAuth callbacks, deep links, etc.)
CapacitorApp.addListener('appUrlOpen', (event) => {
Log.log('iOS app URL open', event.url);
// Handle OAuth callbacks or deep links here
// The URL will be passed to the app when opened via custom scheme
});
}