Skip to content

Commit 9c05f4b

Browse files
mportiz08hqhhuang
andauthored
Inactivate known external links (swiftlang#878) rdar://118834404
Inactivate known external links (swiftlang#878) rdar://118834404 --------- Co-authored-by: Hanqing Huang <[email protected]>
1 parent f128da8 commit 9c05f4b

File tree

9 files changed

+205
-7
lines changed

9 files changed

+205
-7
lines changed

src/components/ContentNode/Reference.vue

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ export default {
5454
name: 'Reference',
5555
computed: {
5656
isInternal({ url }) {
57+
if (!url) {
58+
return false;
59+
}
5760
if (!url.startsWith('/') && !url.startsWith('#')) {
5861
// If the URL has a scheme, it's not an internal link.
5962
return false;
@@ -92,7 +95,7 @@ export default {
9295
props: {
9396
url: {
9497
type: String,
95-
required: true,
98+
required: false,
9699
},
97100
kind: {
98101
type: String,

src/components/ContentNode/ReferenceExternal.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
-->
1010

1111
<template>
12-
<a v-if="isActive" :href="url"><slot /></a>
12+
<a v-if="url && isActive" :href="url"><slot /></a>
1313
<span v-else><slot /></span>
1414
</template>
1515

@@ -19,7 +19,7 @@ export default {
1919
props: {
2020
url: {
2121
type: String,
22-
required: true,
22+
required: false,
2323
},
2424
isActive: {
2525
type: Boolean,

src/components/ContentNode/ReferenceInternal.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
-->
1010

1111
<template>
12-
<router-link v-if="isActive" :to="url"><slot /></router-link>
12+
<router-link v-if="url && isActive" :to="url"><slot /></router-link>
1313
<span v-else><slot/></span>
1414
</template>
1515

@@ -19,7 +19,7 @@ export default {
1919
props: {
2020
url: {
2121
type: String,
22-
required: true,
22+
required: false,
2323
},
2424
isActive: {
2525
type: Boolean,

src/components/Navigator/NavigatorDataProvider.vue

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
<script>
1212
import { fetchIndexPathsData } from 'docc-render/utils/data';
1313
import { flattenNestedData } from 'docc-render/utils/navigatorData';
14+
import AppStore from 'docc-render/stores/AppStore';
1415
import Language from 'docc-render/constants/Language';
1516

1617
/**
@@ -88,11 +89,16 @@ export default {
8889
async fetchIndexData() {
8990
try {
9091
this.isFetching = true;
91-
const { interfaceLanguages, references } = await fetchIndexPathsData(
92+
const {
93+
includedArchiveIdentifiers = [],
94+
interfaceLanguages,
95+
references,
96+
} = await fetchIndexPathsData(
9297
{ slug: this.$route.params.locale || '' },
9398
);
9499
this.navigationIndex = Object.freeze(interfaceLanguages);
95100
this.navigationReferences = Object.freeze(references);
101+
AppStore.setIncludedArchiveIdentifiers(includedArchiveIdentifiers);
96102
} catch (e) {
97103
this.errorFetching = true;
98104
} finally {

src/mixins/referencesProvider.js

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
* See https://swift.org/LICENSE.txt for license information
88
* See https://swift.org/CONTRIBUTORS.txt for Swift project authors
99
*/
10+
import AppStore from 'docc-render/stores/AppStore';
1011

1112
export default {
1213
// inject the `store`
@@ -21,8 +22,39 @@ export default {
2122
}),
2223
},
2324
},
25+
data: () => ({ appState: AppStore.state }),
2426
computed: {
2527
// exposes the references for the current page
26-
references: ({ store }) => store.state.references,
28+
references() {
29+
const {
30+
isFromIncludedArchive,
31+
store: {
32+
state: { references: originalRefs = {} },
33+
},
34+
} = this;
35+
// strip the `url` key from refs if their identifier comes from an
36+
// archive that hasn't been included by DocC
37+
return Object.keys(originalRefs).reduce((newRefs, id) => {
38+
const { url, ...refWithoutUrl } = originalRefs[id];
39+
return {
40+
...newRefs,
41+
[id]: isFromIncludedArchive(id) ? originalRefs[id] : refWithoutUrl,
42+
};
43+
}, {});
44+
},
45+
},
46+
methods: {
47+
isFromIncludedArchive(id) {
48+
const { includedArchiveIdentifiers = [] } = this.appState;
49+
// for backwards compatibility purposes, treat all references as being
50+
// from included archives if there is no data for it
51+
if (!includedArchiveIdentifiers.length) {
52+
return true;
53+
}
54+
55+
return includedArchiveIdentifiers.some(archiveId => (
56+
id?.startsWith(`doc://${archiveId}/`)
57+
));
58+
},
2759
},
2860
};

src/stores/AppStore.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,15 @@ export default {
3030
supportsAutoColorScheme,
3131
systemColorScheme: ColorScheme.light,
3232
availableLocales: [],
33+
includedArchiveIdentifiers: [],
3334
},
3435
reset() {
3536
this.state.imageLoadingStrategy = process.env.VUE_APP_TARGET === 'ide'
3637
? ImageLoadingStrategy.eager : ImageLoadingStrategy.lazy;
3738
this.state.preferredColorScheme = Settings.preferredColorScheme || defaultColorScheme;
3839
this.state.supportsAutoColorScheme = supportsAutoColorScheme;
3940
this.state.systemColorScheme = ColorScheme.light;
41+
this.state.includedArchiveIdentifiers = [];
4042
},
4143
setImageLoadingStrategy(strategy) {
4244
this.state.imageLoadingStrategy = strategy;
@@ -59,6 +61,9 @@ export default {
5961
setSystemColorScheme(value) {
6062
this.state.systemColorScheme = value;
6163
},
64+
setIncludedArchiveIdentifiers(value) {
65+
this.state.includedArchiveIdentifiers = value;
66+
},
6267
syncPreferredColorScheme() {
6368
if (!!Settings.preferredColorScheme
6469
&& Settings.preferredColorScheme !== this.state.preferredColorScheme) {

tests/unit/components/Navigator/NavigatorDataProvider.spec.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
import NavigatorDataProvider from '@/components/Navigator/NavigatorDataProvider.vue';
1212
import { shallowMount } from '@vue/test-utils';
13+
import AppStore from 'docc-render/stores/AppStore';
1314
import Language from 'docc-render/constants/Language';
1415
import { TopicTypes } from '@/constants/TopicTypes';
1516
import { fetchIndexPathsData } from '@/utils/data';
@@ -127,7 +128,13 @@ const references = {
127128
foo: { bar: 'bar' },
128129
};
129130

131+
const includedArchiveIdentifiers = [
132+
'foo',
133+
'bar',
134+
];
135+
130136
const response = {
137+
includedArchiveIdentifiers,
131138
interfaceLanguages: {
132139
[Language.swift.key.url]: [
133140
swiftIndexOne,
@@ -174,6 +181,10 @@ describe('NavigatorDataProvider', () => {
174181
jest.clearAllMocks();
175182
});
176183

184+
afterEach(() => {
185+
AppStore.reset();
186+
});
187+
177188
it('fetches data when mounting NavigatorDataProvider', async () => {
178189
expect(fetchIndexPathsData).toHaveBeenCalledTimes(0);
179190
createWrapper();
@@ -552,4 +563,12 @@ describe('NavigatorDataProvider', () => {
552563
},
553564
]);
554565
});
566+
567+
it('sets `includedArchiveIdentifiers` state in the app store', async () => {
568+
expect(AppStore.state.includedArchiveIdentifiers).toEqual([]);
569+
fetchIndexPathsData.mockResolvedValue(response);
570+
createWrapper();
571+
await flushPromises();
572+
expect(AppStore.state.includedArchiveIdentifiers).toEqual(includedArchiveIdentifiers);
573+
});
555574
});
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/**
2+
* This source file is part of the Swift.org open source project
3+
*
4+
* Copyright (c) 2024 Apple Inc. and the Swift project authors
5+
* Licensed under Apache License v2.0 with Runtime Library Exception
6+
*
7+
* See https://swift.org/LICENSE.txt for license information
8+
* See https://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
*/
10+
import { shallowMount } from '@vue/test-utils';
11+
import referencesProvider from 'docc-render/mixins/referencesProvider';
12+
13+
const FakeComponentInner = {
14+
name: 'FakeComponentInner',
15+
props: ['references'],
16+
render() {
17+
return null;
18+
},
19+
};
20+
21+
const FakeComponentOuter = {
22+
name: 'FakeComponentOuter',
23+
mixins: [referencesProvider],
24+
render(createElement) {
25+
return createElement(FakeComponentInner, {
26+
props: {
27+
references: this.references,
28+
},
29+
});
30+
},
31+
};
32+
33+
const aa = {
34+
identifier: 'doc://A/documentation/A/a',
35+
url: '/documentation/A/a',
36+
title: 'A.A',
37+
};
38+
const ab = {
39+
identifier: 'doc://A/documentation/A/b',
40+
url: '/documentation/A/b',
41+
title: 'A.B',
42+
};
43+
const bb = {
44+
identifier: 'doc://B/documentation/B/b',
45+
url: '/documentation/B/b',
46+
title: 'B.B',
47+
};
48+
const bbb = {
49+
identifier: 'doc://BB/documentation/BB/b',
50+
url: '/documentation/BB/b',
51+
title: 'BB.B',
52+
};
53+
54+
const references = {
55+
[aa.identifier]: aa,
56+
[ab.identifier]: ab,
57+
[bb.identifier]: bb,
58+
[bbb.identifier]: bbb,
59+
};
60+
61+
const provide = {
62+
store: {
63+
state: { references },
64+
},
65+
};
66+
67+
const createOuter = (opts = { provide }) => shallowMount(FakeComponentOuter, opts);
68+
69+
describe('referencesProvider', () => {
70+
it('provides a store with a default state', () => {
71+
const outer = createOuter({});
72+
const inner = outer.find(FakeComponentInner);
73+
expect(inner.exists()).toBe(true);
74+
expect(inner.props('references')).toEqual({});
75+
});
76+
77+
it('provides references from a store', () => {
78+
const outer = createOuter();
79+
const inner = outer.find(FakeComponentInner);
80+
expect(inner.exists()).toBe(true);
81+
expect(inner.props('references')).toEqual(references);
82+
});
83+
84+
it('removes `url` data for refs with non-empty `includedArchiveIdentifiers` app state', () => {
85+
// empty `includedArchiveIdentifiers` — no changes to refs
86+
const outer = createOuter();
87+
let inner = outer.find(FakeComponentInner);
88+
expect(inner.exists()).toBe(true);
89+
expect(inner.props('references')).toEqual(references);
90+
91+
// `includedArchiveIdentifiers` contains all refs - no changes to refs
92+
outer.setData({
93+
appState: {
94+
includedArchiveIdentifiers: ['A', 'B', 'BB'],
95+
},
96+
});
97+
inner = outer.find(FakeComponentInner);
98+
expect(inner.exists()).toBe(true);
99+
expect(inner.props('references')).toEqual(references);
100+
101+
// `includedArchiveIdentifiers` only contains archive B — remove `url` field
102+
// from all non-B refs
103+
outer.setData({
104+
appState: {
105+
includedArchiveIdentifiers: ['B'],
106+
},
107+
});
108+
inner = outer.find(FakeComponentInner);
109+
expect(inner.exists()).toBe(true);
110+
const refs3 = inner.props('references');
111+
expect(refs3).not.toEqual(references);
112+
expect(refs3[aa.identifier].title).toBe(aa.title);
113+
expect(refs3[aa.identifier].url).toBeFalsy(); // aa `url` is gone now
114+
expect(refs3[ab.identifier].title).toBe(ab.title);
115+
expect(refs3[ab.identifier].url).toBeFalsy(); // ab `url` is gone now
116+
expect(refs3[bb.identifier].title).toBe(bb.title);
117+
expect(refs3[bb.identifier].url).toBe(bb.url); // bb still has `url`
118+
expect(refs3[bbb.identifier].title).toBe(bbb.title);
119+
expect(refs3[bbb.identifier].url).toBeFalsy(); // bbb `url` is gone now
120+
});
121+
});

tests/unit/stores/AppStore.spec.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ describe('AppStore', () => {
2222
systemColorScheme: ColorScheme.light,
2323
preferredLocale: null,
2424
availableLocales: [],
25+
includedArchiveIdentifiers: [],
2526
});
2627
});
2728

@@ -37,6 +38,7 @@ describe('AppStore', () => {
3738
systemColorScheme: ColorScheme.light,
3839
preferredLocale: null,
3940
availableLocales: [],
41+
includedArchiveIdentifiers: [],
4042
});
4143

4244
// restore target
@@ -72,11 +74,20 @@ describe('AppStore', () => {
7274
});
7375
});
7476

77+
describe('setIncludedArchiveIdentifiers', () => {
78+
it('sets the included archive identifiers', () => {
79+
const includedArchiveIdentifiers = ['foo', 'bar'];
80+
AppStore.setIncludedArchiveIdentifiers(includedArchiveIdentifiers);
81+
expect(AppStore.state.includedArchiveIdentifiers).toEqual(includedArchiveIdentifiers);
82+
});
83+
});
84+
7585
it('resets the state', () => {
7686
AppStore.setImageLoadingStrategy(ImageLoadingStrategy.eager);
7787
AppStore.setPreferredColorScheme(ColorScheme.auto);
7888
AppStore.setSystemColorScheme(ColorScheme.dark);
7989
AppStore.syncPreferredColorScheme();
90+
AppStore.setIncludedArchiveIdentifiers(['a']);
8091
AppStore.reset();
8192

8293
expect(AppStore.state).toEqual({
@@ -86,6 +97,7 @@ describe('AppStore', () => {
8697
systemColorScheme: ColorScheme.light,
8798
preferredLocale: null,
8899
availableLocales: [],
100+
includedArchiveIdentifiers: [],
89101
});
90102
});
91103
});

0 commit comments

Comments
 (0)