Compare commits

...
17 changed files with 187 additions and 292 deletions

No files matched your search

@@ -3,7 +3,3 @@
.nativeDragRegion {
-webkit-app-region: none;
}
:global(html.platform-native.platform-macos) .nativeDragRegion {
-webkit-app-region: drag;
}
@@ -1,10 +1,6 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {
buildMaskedLink,
canWrapSelectionAsLink,
parsePastedUrl,
} from '@app/features/lexical/composer/ComposerLinkPaste';
import {resolvePastedLinkInsertion} from '@app/features/lexical/composer/ComposerLinkPaste';
import {
$createComposerSegmentNodes,
$projectComposer,
@@ -13,6 +9,7 @@ import {
import {$captureSelectionOffsets, $selectComposerOffset} from '@app/features/lexical/composer/composerOffsets';
import {$isSlashSlotNode, type SlashSlotNode} from '@app/features/lexical/composer/nodes/SlashSlotNode';
import {COMPOSER_SLASH_SLOT_STATE_MAX_ID_LENGTH} from '@app/features/lexical/composer/SlashSlotPersistence';
import {ParserFlags} from '@app/features/messaging/utils/markdown/parser/Enums';
import type {MentionSegment} from '@app/features/messaging/utils/TextareaSegmentManager';
import {mergeRegister} from '@lexical/utils';
import {
@@ -26,6 +23,7 @@ import {
$isNodeSelection,
$isRangeSelection,
$isTextNode,
COMMAND_PRIORITY_CRITICAL,
COMMAND_PRIORITY_NORMAL,
COPY_COMMAND,
CUT_COMMAND,
@@ -65,6 +63,7 @@ export interface ComposerClipboardSelection extends ComposerClipboardSlice {
export interface ComposerClipboardCommandState {
getPlainText(): boolean;
isEditable(): boolean;
getMarkdownParserFlags(): number;
}
interface SerializedComposerClipboardPayload {
@@ -468,31 +467,30 @@ export function registerComposerClipboardCommands(
editor.registerCommand(
PASTE_COMMAND,
(event) => {
if (!state.isEditable() || state.getPlainText()) {
if (!state.isEditable() || (state.getMarkdownParserFlags() & ParserFlags.ALLOW_MASKED_LINKS) === 0) {
return false;
}
const clipboardEvent = getClipboardEvent(event);
if (clipboardEvent == null || clipboardEvent.clipboardData == null) {
return false;
}
const url = parsePastedUrl(clipboardEvent.clipboardData.getData('text/plain'));
if (url == null) {
return false;
}
const selection = $getSelection();
if (!$isRangeSelection(selection) || selection.isCollapsed()) {
return false;
}
const selectedText = selection.getTextContent();
if (!canWrapSelectionAsLink(selectedText)) {
const replacement = resolvePastedLinkInsertion(
clipboardEvent.clipboardData.getData('text/plain'),
selection.getTextContent(),
);
if (replacement === null) {
return false;
}
selection.insertText(buildMaskedLink(selectedText, url));
selection.insertText(replacement);
$addUpdateTag(PASTE_TAG);
clipboardEvent.preventDefault();
return true;
},
COMMAND_PRIORITY_NORMAL,
COMMAND_PRIORITY_CRITICAL,
),
editor.registerCommand(
COPY_COMMAND,
@@ -4,7 +4,12 @@ import {
buildMaskedLink,
canWrapSelectionAsLink,
parsePastedUrl,
resolvePastedLinkInsertion,
} from '@app/features/lexical/composer/ComposerLinkPaste';
import {DEFAULT_COMPOSER_MARKDOWN_FLAGS} from '@app/features/lexical/composer/markdownSpans';
import {MarkdownContext} from '@app/features/messaging/components/markdown/renderers/RendererTypes';
import {getParserFlagsForContext} from '@app/features/messaging/utils/markdown/MarkdownParserFlags';
import {ParserFlags} from '@app/features/messaging/utils/markdown/parser/Enums';
import {describe, expect, it} from 'vitest';
describe('parsePastedUrl', () => {
@@ -60,3 +65,42 @@ describe('buildMaskedLink', () => {
);
});
});
describe('resolvePastedLinkInsertion', () => {
it('wraps a selection when a bare url is pasted over it', () => {
expect(resolvePastedLinkInsertion('https://fluxer.app/download', 'the download page')).toBe(
'[the download page](<https://fluxer.app/download>)',
);
});
it('wraps a message link the same way a plain url is wrapped', () => {
const messageLink = 'https://fluxer.app/channels/1234567890/9876543210/1122334455';
expect(resolvePastedLinkInsertion(messageLink, 'this message')).toBe(`[this message](<${messageLink}>)`);
});
it('declines when the pasted text is not a url', () => {
expect(resolvePastedLinkInsertion('just some text', 'selected')).toBeNull();
});
it('declines when the selection cannot be wrapped', () => {
expect(resolvePastedLinkInsertion('https://fluxer.app', ' ')).toBeNull();
expect(resolvePastedLinkInsertion('https://fluxer.app', 'https://example.test')).toBeNull();
});
it('declines a non-web protocol so pasting cannot forge a link', () => {
expect(resolvePastedLinkInsertion('javascript:alert(1)', 'click me')).toBeNull();
});
});
describe('masked link support across composer surfaces', () => {
const surfaces: Array<[string, number]> = [
['channel composer', DEFAULT_COMPOSER_MARKDOWN_FLAGS],
['message edit / forward', getParserFlagsForContext(MarkdownContext.STANDARD_WITH_JUMBO)],
['channel topic', getParserFlagsForContext(MarkdownContext.STANDARD_WITHOUT_JUMBO)],
['user bio', getParserFlagsForContext(MarkdownContext.RESTRICTED_USER_BIO)],
];
it.each(surfaces)('%s allows masked links, so pasting a link must wrap', (_name, flags) => {
expect(flags & ParserFlags.ALLOW_MASKED_LINKS).not.toBe(0);
});
});
@@ -43,3 +43,11 @@ export function canWrapSelectionAsLink(selectedText: string): boolean {
export function buildMaskedLink(selectedText: string, url: string): string {
return `[${selectedText}](<${url}>)`;
}
export function resolvePastedLinkInsertion(pastedText: string, selectedText: string): string | null {
const url = parsePastedUrl(pastedText);
if (url === null || !canWrapSelectionAsLink(selectedText)) {
return null;
}
return buildMaskedLink(selectedText, url);
}
@@ -33,6 +33,7 @@ import {
$wrapComposerSelection,
} from '@app/features/lexical/composer/composerOffsets';
import styles from '@app/features/lexical/composer/LexicalMessageComposer.module.css';
import {DEFAULT_COMPOSER_MARKDOWN_FLAGS} from '@app/features/lexical/composer/markdownSpans';
import {ComposerCommandNode} from '@app/features/lexical/composer/nodes/ComposerCommandNode';
import {ComposerCustomEmojiNode} from '@app/features/lexical/composer/nodes/ComposerCustomEmojiNode';
import {ComposerMentionNode} from '@app/features/lexical/composer/nodes/ComposerMentionNode';
@@ -263,6 +264,8 @@ const ComposerInner = ({
submitOnEnterRef.current = submitOnEnter;
const plainTextRef = useRef(plainText);
plainTextRef.current = plainText;
const markdownParserFlagsRef = useRef(markdownParserFlags ?? DEFAULT_COMPOSER_MARKDOWN_FLAGS);
markdownParserFlagsRef.current = markdownParserFlags ?? DEFAULT_COMPOSER_MARKDOWN_FLAGS;
const disabledRef = useRef(disabled);
disabledRef.current = disabled;
const onSlashCommandStateChangeRef = useRef(onSlashCommandStateChange);
@@ -607,6 +610,7 @@ const ComposerInner = ({
registerComposerClipboardCommands(editor, {
getPlainText: () => plainTextRef.current,
isEditable: () => !disabledRef.current && editor.isEditable(),
getMarkdownParserFlags: () => markdownParserFlagsRef.current,
}),
[editor],
);
@@ -156,6 +156,7 @@ export const NativeTitleBarControl = observer(() => {
},
[i18n, updateDesktopWindowBehavior],
);
if (getElectronAPI()?.platform === 'darwin') return null;
return (
<Switch
ariaLabel={i18n._(USE_NATIVE_TITLE_BAR_DESCRIPTOR)}
@@ -2,7 +2,7 @@
import {PRODUCT_NAME} from '@app/features/app/config/I18nDisplayConstants';
import {CLOSE_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors';
import {getElectronAPI, isDesktop} from '@app/features/ui/utils/NativeUtils';
import {getElectronAPI, getNativePlatformSync, isDesktop, isNativeMacOS} from '@app/features/ui/utils/NativeUtils';
import type {SearchableSettingDescriptor} from '@app/features/user/components/settings_utils/search_index/SearchIndexTypes';
import {BACKGROUND_DESCRIPTOR} from '@app/features/user/components/settings_utils/search_index/SharedDescriptors';
import {msg} from '@lingui/core/macro';
@@ -283,7 +283,7 @@ export const desktopSettingsIndex: Array<SearchableSettingDescriptor> = [
],
description: USE_THE_OPERATING_SYSTEM_S_WINDOW_CHROME_DESCRIPTOR,
tags: ['desktop', 'appearance'],
isVisible: isDesktop,
isVisible: () => isDesktop() && !isNativeMacOS(getNativePlatformSync()),
},
{
id: 'advanced-tray-icon',
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {getDesktopWindowBehaviorSettings} from '@app/features/ui/utils/DesktopWindowBehaviorUtils';
import {isDesktop} from '@app/features/ui/utils/NativeUtils';
import {getNativePlatformSync, isDesktop, isNativeMacOS} from '@app/features/ui/utils/NativeUtils';
import {useEffect, useState} from 'react';
const TITLEBAR_HEIGHT_CSS_VAR = '--native-titlebar-height';
@@ -9,8 +9,12 @@ const CUSTOM_TITLEBAR_HEIGHT = '32px';
const NATIVE_TITLEBAR_HEIGHT = '0px';
const NATIVE_SYSTEM_TITLEBAR_CLASS = 'native-system-titlebar';
function supportsSystemTitleBar(): boolean {
return isDesktop() && !isNativeMacOS(getNativePlatformSync());
}
function getInitialUseSystemTitleBar(): boolean {
return isDesktop() && document.documentElement.classList.contains(NATIVE_SYSTEM_TITLEBAR_CLASS);
return supportsSystemTitleBar() && document.documentElement.classList.contains(NATIVE_SYSTEM_TITLEBAR_CLASS);
}
export function useNativeTitleBar(): boolean {
@@ -19,6 +23,10 @@ export function useNativeTitleBar(): boolean {
if (!isDesktop()) {
return;
}
if (!supportsSystemTitleBar()) {
setUseSystemTitleBar(false);
return;
}
let mounted = true;
void getDesktopWindowBehaviorSettings().then((settings) => {
if (!mounted || !settings) {
+1 -1
View File
@@ -25,7 +25,7 @@
"devDependencies": {
"@types/node": "24.10.9",
"@typescript/native-preview": "7.0.0-dev.20260130.1",
"electron": "43.4.1",
"electron": "43.4.0",
"electron-builder": "26.9.0",
"esbuild": "0.27.2",
"typescript": "5.9.3"
+2 -2
View File
@@ -128,7 +128,7 @@ function consumeWaylandPortalSurfacePreference(): DisplayMediaPortalSurfacePrefe
return preference;
}
export const DISPLAY_MEDIA_PORTAL_EMPTY_CHANNEL = 'display-media-portal-empty';
const DISPLAY_MEDIA_PORTAL_EMPTY_CHANNEL = 'display-media-portal-empty';
type WaylandPortalUnavailableReason = 'empty' | 'error';
@@ -152,7 +152,7 @@ async function defaultWaylandPortalSourceProvider(
return desktopCapturer.getSources({types});
}
export async function resolveWaylandPortalDisplayMedia(
async function resolveWaylandPortalDisplayMedia(
context: WaylandPortalResolutionContext,
): Promise<Electron.Streams | null> {
const {requestId, preference, getSources, notifyUnavailable} = context;
+47 -3
View File
@@ -148,6 +148,31 @@ function buildDesktopFileContents(execPath: string, hidden: boolean): string {
].join('\n');
}
function readDesktopEntryValue(contents: string, key: string): string | null {
for (const line of contents.split('\n')) {
const trimmed = line.trim();
if (trimmed.startsWith('[Desktop Action ')) break;
if (trimmed.startsWith(`${key}=`)) return trimmed.slice(key.length + 1).trim();
}
return null;
}
function isExecutableFile(candidate: string): boolean {
try {
if (!fs.statSync(candidate).isFile()) return false;
fs.accessSync(candidate, fs.constants.X_OK);
return true;
} catch {
return false;
}
}
function isStaleDesktopFile(contents: string): boolean {
const tryExec = readDesktopEntryValue(contents, 'TryExec');
if (tryExec === null || !tryExec.startsWith('/')) return false;
return !isExecutableFile(tryExec);
}
function readExistingDesktopFile(filePath: string): string | null {
try {
return fs.readFileSync(filePath, 'utf8');
@@ -213,10 +238,29 @@ export function ensureLinuxProtocolDesktopEntry(): void {
}
if (existing !== null) {
if (!existing.includes(GENERATED_MARKER)) {
logger.debug('Linux .desktop entry was hand-edited; leaving untouched', {filePath});
return;
if (!isStaleDesktopFile(existing)) {
logger.debug('Linux .desktop entry was hand-edited; leaving untouched', {filePath});
return;
}
const systemEntry = findSystemDesktopEntry();
if (systemEntry) {
try {
fs.unlinkSync(filePath);
logger.info('Removed stale .desktop entry shadowing the system entry', {filePath, systemEntry});
} catch (error) {
logger.warn('Failed to remove stale .desktop entry', {filePath, error});
}
try {
app.setAsDefaultProtocolClient(APP_PROTOCOL);
} catch (error) {
logger.warn('Failed to register protocol client', {error});
}
return;
}
logger.info('Rewriting stale .desktop entry whose TryExec no longer resolves', {filePath});
} else {
needsWrite = existing !== desired;
}
needsWrite = existing !== desired;
}
if (!needsWrite) {
logger.debug('Linux .desktop entry already up to date', {filePath});
@@ -1,167 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import assert from 'node:assert/strict';
import test from 'node:test';
interface FakeCommandLineState {
switches: Map<string, string>;
}
function createFakeCommandLine(initial: Record<string, string | true> = {}) {
const state: FakeCommandLineState = {switches: new Map()};
for (const [name, value] of Object.entries(initial)) {
state.switches.set(name, value === true ? '' : value);
}
return {
state,
appendSwitch(name: string, value?: string): void {
state.switches.set(name, value ?? '');
},
getSwitchValue(name: string): string {
return state.switches.get(name) ?? '';
},
hasSwitch(name: string): boolean {
return state.switches.has(name);
},
};
}
const electronCommandLine = createFakeCommandLine();
test.mock.module('electron', {
namedExports: {
app: {commandLine: electronCommandLine},
desktopCapturer: {getSources: async () => []},
ipcMain: {handle: () => {}, on: () => {}},
screen: {getAllDisplays: () => []},
BrowserWindow: {getAllWindows: () => []},
},
});
test.mock.module('electron-log', {
defaultExport: {
info: () => {},
warn: () => {},
error: () => {},
debug: () => {},
},
});
const {addLinuxScreenCapturePipeWireFeature, appendLinuxOzonePlatformHint, hasConfiguredOzonePlatformSwitch} =
await import('./ChromiumRuntime');
const {resolveWaylandPortalDisplayMedia, DISPLAY_MEDIA_PORTAL_EMPTY_CHANNEL} = await import('./DisplayMedia');
function withPlatform<T>(platform: NodeJS.Platform, run: () => T): T {
const original = Object.getOwnPropertyDescriptor(process, 'platform');
Object.defineProperty(process, 'platform', {configurable: true, value: platform});
try {
return run();
} finally {
if (original) {
Object.defineProperty(process, 'platform', original);
}
}
}
test('addLinuxScreenCapturePipeWireFeature enables WebRTCPipeWireCapturer only on Linux', () => {
withPlatform('linux', () => {
const features = new Set<string>();
addLinuxScreenCapturePipeWireFeature(features);
assert.ok(features.has('WebRTCPipeWireCapturer'));
});
withPlatform('darwin', () => {
const features = new Set<string>();
addLinuxScreenCapturePipeWireFeature(features);
assert.equal(features.has('WebRTCPipeWireCapturer'), false);
});
withPlatform('win32', () => {
const features = new Set<string>();
addLinuxScreenCapturePipeWireFeature(features);
assert.equal(features.has('WebRTCPipeWireCapturer'), false);
});
});
test('appendLinuxOzonePlatformHint appends ozone-platform-hint=auto on Linux', () => {
withPlatform('linux', () => {
const commandLine = createFakeCommandLine();
appendLinuxOzonePlatformHint(commandLine);
assert.equal(commandLine.getSwitchValue('ozone-platform-hint'), 'auto');
assert.ok(commandLine.hasSwitch('ozone-platform-hint'));
});
});
test('appendLinuxOzonePlatformHint does not append on darwin or win32', () => {
withPlatform('darwin', () => {
const commandLine = createFakeCommandLine();
appendLinuxOzonePlatformHint(commandLine);
assert.equal(commandLine.hasSwitch('ozone-platform-hint'), false);
});
withPlatform('win32', () => {
const commandLine = createFakeCommandLine();
appendLinuxOzonePlatformHint(commandLine);
assert.equal(commandLine.hasSwitch('ozone-platform-hint'), false);
});
});
test('appendLinuxOzonePlatformHint does not override a user-supplied --ozone-platform', () => {
withPlatform('linux', () => {
const commandLine = createFakeCommandLine({'ozone-platform': 'x11'});
assert.ok(hasConfiguredOzonePlatformSwitch(commandLine));
appendLinuxOzonePlatformHint(commandLine);
assert.equal(commandLine.hasSwitch('ozone-platform-hint'), false);
assert.equal(commandLine.getSwitchValue('ozone-platform'), 'x11');
});
});
test('appendLinuxOzonePlatformHint respects a user-supplied bare --ozone-platform switch', () => {
withPlatform('linux', () => {
const commandLine = createFakeCommandLine({'ozone-platform': true});
assert.ok(hasConfiguredOzonePlatformSwitch(commandLine));
appendLinuxOzonePlatformHint(commandLine);
assert.equal(commandLine.hasSwitch('ozone-platform-hint'), false);
});
});
test('resolveWaylandPortalDisplayMedia returns the first source when the portal provides one', async () => {
const source = {id: 'screen:0:0', name: 'Screen 1'} as Electron.DesktopCapturerSource;
const notifications: Array<{requestId: string; reason: string}> = [];
const streams = await resolveWaylandPortalDisplayMedia({
requestId: 'req-1',
preference: 'monitor',
getSources: async () => [source],
notifyUnavailable: (requestId, reason) => notifications.push({requestId, reason}),
});
assert.deepEqual(streams, {video: source});
assert.equal(notifications.length, 0);
});
test('resolveWaylandPortalDisplayMedia signals an empty portal result without a video stream', async () => {
const notifications: Array<{requestId: string; reason: string}> = [];
const streams = await resolveWaylandPortalDisplayMedia({
requestId: 'req-2',
preference: 'monitor',
getSources: async () => [],
notifyUnavailable: (requestId, reason) => notifications.push({requestId, reason}),
});
assert.equal(streams, null);
assert.deepEqual(notifications, [{requestId: 'req-2', reason: 'empty'}]);
});
test('resolveWaylandPortalDisplayMedia signals a portal error when getSources rejects', async () => {
const notifications: Array<{requestId: string; reason: string}> = [];
const streams = await resolveWaylandPortalDisplayMedia({
requestId: 'req-3',
preference: 'window',
getSources: async () => {
throw new Error('xdg-desktop-portal unavailable');
},
notifyUnavailable: (requestId, reason) => notifications.push({requestId, reason}),
});
assert.equal(streams, null);
assert.deepEqual(notifications, [{requestId: 'req-3', reason: 'error'}]);
});
test('the Wayland portal-unavailable IPC channel matches the wired renderer channel', () => {
assert.equal(DISPLAY_MEDIA_PORTAL_EMPTY_CHANNEL, 'display-media-portal-empty');
});
@@ -1,92 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import assert from 'node:assert/strict';
import test from 'node:test';
import {
isFluxerGameCaptureVulkanLayerValue,
shouldRemoveStaleFluxerGameCaptureVulkanLayerValue,
} from './WindowsVulkanGameCaptureLayer';
const INSTALLED_MANIFEST_PATH =
'C:\\Users\\hampus\\AppData\\Local\\fluxer_desktop_canary\\current\\resources\\app.asar.unpacked\\node_modules\\@fluxer\\win-game-capture\\fluxer-vulkan-layer.win32-arm64-msvc.json';
const DEV_TREE_MANIFEST_PATH =
'C:\\t\\fluxer\\fluxer_desktop\\native\\win-game-capture\\fluxer-vulkan-layer.win32-arm64-msvc.json';
const RETIRED_PACKAGE_MANIFEST_PATH =
'C:\\Users\\hampus\\AppData\\Local\\fluxer_desktop\\current\\resources\\app.asar.unpacked\\node_modules\\@fluxer\\win-screen-capture\\fluxer-vulkan-layer.win32-arm64-msvc.json';
test('isFluxerGameCaptureVulkanLayerValue matches the installed @fluxer/win-game-capture manifest', () => {
assert.ok(isFluxerGameCaptureVulkanLayerValue(INSTALLED_MANIFEST_PATH));
});
test('isFluxerGameCaptureVulkanLayerValue matches a dev/build tree manifest outside node_modules/@fluxer', () => {
assert.ok(isFluxerGameCaptureVulkanLayerValue(DEV_TREE_MANIFEST_PATH));
});
test('isFluxerGameCaptureVulkanLayerValue matches the retired win-screen-capture package name', () => {
assert.ok(isFluxerGameCaptureVulkanLayerValue(RETIRED_PACKAGE_MANIFEST_PATH));
});
test('isFluxerGameCaptureVulkanLayerValue matches every supported architecture and registry slash style', () => {
for (const arch of ['x64', 'ia32', 'arm64']) {
assert.ok(
isFluxerGameCaptureVulkanLayerValue(
`C:\\t\\fluxer\\native\\win-game-capture\\fluxer-vulkan-layer.win32-${arch}-msvc.json`,
),
arch,
);
}
assert.ok(
isFluxerGameCaptureVulkanLayerValue(
'C:/t/fluxer/fluxer_desktop/native/win-game-capture/Fluxer-Vulkan-Layer.win32-ARM64-msvc.json',
),
);
});
test('isFluxerGameCaptureVulkanLayerValue never matches another vendor Vulkan implicit layer', () => {
const foreignValues = [
'C:\\Program Files\\NVIDIA Corporation\\Nsight Graphics\\nvoglv64.json',
'C:\\Program Files (x86)\\Steam\\steamoverlayvulkanlayer.json',
'C:\\Program Files\\obs-studio\\data\\obs-plugins\\win-capture\\obs-vulkan64.json',
'C:\\Windows\\System32\\VkLayer_khronos_validation.json',
'C:\\Program Files\\OtherVendor\\layers\\fluxer-vulkan-layer.win32-x64-msvc.json',
'C:\\t\\fluxer\\fluxer_desktop\\native\\win-game-capture\\othervendor-vulkan-layer.json',
'C:\\t\\fluxer\\fluxer_desktop\\native\\linux-screen-capture\\fluxer-vulkan-layer.win32-x64-msvc.json',
'C:\\t\\fluxer\\fluxer_desktop\\native\\mac-screen-capture\\fluxer-vulkan-layer.win32-x64-msvc.json',
'C:\\t\\fluxer\\fluxer_desktop\\native\\win-game-capture\\fluxer-vulkan-layer.win32-arm64-msvc.dll',
];
for (const valueName of foreignValues) {
assert.equal(isFluxerGameCaptureVulkanLayerValue(valueName), false, valueName);
}
});
test('shouldRemoveStaleFluxerGameCaptureVulkanLayerValue keeps the live registration of the running install', () => {
assert.equal(
shouldRemoveStaleFluxerGameCaptureVulkanLayerValue(INSTALLED_MANIFEST_PATH, INSTALLED_MANIFEST_PATH),
false,
);
assert.equal(
shouldRemoveStaleFluxerGameCaptureVulkanLayerValue(
INSTALLED_MANIFEST_PATH.replace(/\\/g, '/').toUpperCase(),
INSTALLED_MANIFEST_PATH,
),
false,
);
});
test('shouldRemoveStaleFluxerGameCaptureVulkanLayerValue removes orphaned registrations alongside the kept one', () => {
assert.ok(shouldRemoveStaleFluxerGameCaptureVulkanLayerValue(DEV_TREE_MANIFEST_PATH, INSTALLED_MANIFEST_PATH));
assert.ok(shouldRemoveStaleFluxerGameCaptureVulkanLayerValue(RETIRED_PACKAGE_MANIFEST_PATH, INSTALLED_MANIFEST_PATH));
});
test('shouldRemoveStaleFluxerGameCaptureVulkanLayerValue removes every Fluxer layer when nothing is kept', () => {
for (const valueName of [INSTALLED_MANIFEST_PATH, DEV_TREE_MANIFEST_PATH, RETIRED_PACKAGE_MANIFEST_PATH]) {
assert.ok(shouldRemoveStaleFluxerGameCaptureVulkanLayerValue(valueName, null), valueName);
}
});
test('shouldRemoveStaleFluxerGameCaptureVulkanLayerValue never removes a foreign vendor layer, kept path or not', () => {
const foreignValue = 'C:\\Program Files (x86)\\Steam\\steamoverlayvulkanlayer.json';
assert.equal(shouldRemoveStaleFluxerGameCaptureVulkanLayerValue(foreignValue, null), false);
assert.equal(shouldRemoveStaleFluxerGameCaptureVulkanLayerValue(foreignValue, INSTALLED_MANIFEST_PATH), false);
});
@@ -1170,6 +1170,29 @@ pub(crate) fn is_block_start(line: &str, flags: u32) -> bool {
|| is_blockquote_start(line, flags)
}
pub(crate) fn is_table_start(lines: &[Line], index: usize, flags: u32) -> bool {
if !ParserFlags::has(flags, ParserFlags::ALLOW_TABLES) || index + 2 >= lines.len() {
return false;
}
let header_line = trim_table_line(&lines[index]);
let align_line = trim_table_line(&lines[index + 1]);
if !header_line.text.contains('|') || !align_line.text.contains('|') {
return false;
}
let header_cells = split_table_cells(&header_line);
if header_cells.is_empty() || !cells_have_content(&header_cells) {
return false;
}
let Some(alignments) = parse_alignments(&split_table_cells(&align_line)) else {
return false;
};
if header_cells.len() != alignments.len() {
return false;
}
let body_line = trim_table_line(&lines[index + 2]);
body_line.text.contains('|') && !is_table_block_break(&body_line.text)
}
pub(crate) fn is_heading_start(line: &str, flags: u32) -> bool {
if !ParserFlags::has(flags, ParserFlags::ALLOW_HEADINGS) || !starts_with(line, "#") {
return false;
@@ -159,6 +159,11 @@ impl RuntimeState<'_> {
let next_line = &self.lines[self.current_line + consumed].text;
let trimmed_next = trim_start(next_line);
if crate::block::is_block_start(trimmed_next, self.parser.flags())
|| crate::block::is_table_start(
&self.lines,
self.current_line + consumed,
self.parser.flags(),
)
|| trimmed_next.is_empty()
{
break;
@@ -404,3 +404,26 @@ proptest::proptest! {
prop_assert!(serialized.starts_with('['));
}
}
#[test]
fn native_parser_starts_a_table_directly_after_a_text_line() {
let input =
"intro text\r\n| Framework | Type |\r\n| --------- | ---- |\r\n| React | UI library |";
let parsed = parse(input, ParserFlags::ALL, "");
let nodes = parsed["nodes"].as_array().expect("nodes array");
assert!(
nodes.iter().any(|node| node["type"] == "Table"),
"expected a Table node, got {parsed}"
);
}
#[test]
fn native_parser_still_requires_a_delimiter_row_for_a_table() {
let input = "intro text\n| not | a | table |\njust more text";
let parsed = parse(input, ParserFlags::ALL, "");
let nodes = parsed["nodes"].as_array().expect("nodes array");
assert!(
!nodes.iter().any(|node| node["type"] == "Table"),
"pipes alone must not form a table, got {parsed}"
);
}
+5 -5
View File
@@ -1674,8 +1674,8 @@ importers:
specifier: 7.0.0-dev.20260130.1
version: 7.0.0-dev.20260130.1
electron:
specifier: 43.4.1
version: 43.4.1
specifier: 43.4.0
version: 43.4.0
electron-builder:
specifier: 26.9.0
version: 26.9.0([email protected])
@@ -8216,8 +8216,8 @@ packages:
resolution: {integrity: sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==}
engines: {node: '>=8.0.0'}
[email protected].1:
resolution: {integrity: sha512-5b+EuiwkgG5iRcsEL34rimgRpkYp15SsfZOa0pC5kXs0Tb82TH4n95rpQzTZa7yRCbA7tm0WoEbuBL6NaAhAcA==}
[email protected].0:
resolution: {integrity: sha512-3qxGF0CeQbiox5oWV1JlbWGQ1VerbmDhTFqW4sJ8h7uqTHniFYPObXJcDna0DMh32et0fFyKzz0YY8lJv3t5jg==}
engines: {node: '>= 22.12.0'}
hasBin: true
@@ -18308,7 +18308,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
[email protected].1:
[email protected].0:
dependencies:
'@electron-internal/extract-zip': 1.0.5
'@electron/get': 5.1.0